1use std::collections::{BTreeMap, HashMap, HashSet};
4
5use crate::{DeltaFunnelError, error::DeltaSourceSchemaSnafu, observability};
6use delta_kernel::object_store::aws::AmazonS3ConfigKey;
7
8mod deletion_vector;
9mod kernel;
10mod protocol;
11mod read;
12mod snapshot;
13#[cfg(test)]
14pub(crate) mod test_support;
15mod uri;
16
17use super::validate_table_source_names;
18#[allow(unused_imports)]
19pub(crate) use deletion_vector::{
20 KernelDeletionVectorReadRequest, KernelDeletionVectorReader, KernelDeletionVectorReaderConfig,
21 ProviderDeletionVectorSelection, ProviderDeletionVectorSelectionContext,
22};
23#[cfg(test)]
24pub(crate) use kernel::KernelStructType;
25use kernel::{ArrowSchemaRef, Version, snapshot_arrow_schema};
26pub(crate) use kernel::{
27 ColumnName as KernelColumnName, DecimalData as KernelDecimalData,
28 DeltaKernelPartitionScalarAdapterError, DeltaKernelPredicate, KernelColumnMetadataKey,
29 KernelDataType, KernelMetadataColumnSpec, KernelMetadataValue, KernelPrimitiveType,
30 KernelSchemaRef, KernelStructField, Scalar as KernelScalar,
31 arrow_partition_type_to_kernel_primitive, datafusion_expr_to_kernel_predicate,
32 kernel_partition_scalar_to_datafusion_scalar,
33};
34pub(crate) use protocol::preflight_delta_protocol_with_tracing;
35pub use protocol::{ProtocolPreflight, preflight_delta_protocol, preflight_delta_sources};
36pub(crate) use read::{
37 KernelDataFilePredicateEvalRequest, KernelDataFileReadRequest, KernelDataFileReader,
38 KernelDataFileReaderConfig, KernelDataFileTransformRequest, KernelScanReadSchema,
39};
40use snapshot::{LoadedDeltaTableSnapshot, load_delta_table_snapshot, s3_auth_mode_hint_for_source};
41
42#[allow(dead_code)]
49pub(crate) struct KernelScanMetadataExpansion {
50 pub(crate) files: Vec<KernelScanFileMetadata>,
52 pub(crate) scan_metadata_exhausted: bool,
58}
59
60#[allow(dead_code)]
62pub(crate) struct KernelScanFileMetadata {
63 pub(crate) path: String,
68 pub(crate) size: i64,
70 pub(crate) modification_time: i64,
75 pub(crate) stats: Option<KernelScanFileStats>,
77 pub(crate) deletion_vector: KernelScanDeletionVectorMetadata,
83 pub(crate) physical_to_logical_transform: KernelPhysicalToLogicalTransform,
89 pub(crate) partition_values: HashMap<String, String>,
93}
94
95#[allow(dead_code)]
97pub(crate) struct KernelScanFileStats {
98 pub(crate) num_records: u64,
100}
101
102#[allow(dead_code)]
107#[derive(Clone)]
108pub(crate) enum KernelScanDeletionVectorMetadata {
109 NotPresent,
111 Present(KernelScanDeletionVectorHandle),
113}
114
115#[allow(dead_code)]
121#[derive(Clone)]
122pub(crate) struct KernelScanDeletionVectorHandle {
123 dv_info: kernel::DvInfo,
125}
126
127impl KernelScanDeletionVectorHandle {
128 #[cfg(test)]
129 pub(crate) fn from_test_dv_info(dv_info: kernel::DvInfo) -> Self {
130 Self { dv_info }
131 }
132}
133
134impl KernelScanDeletionVectorMetadata {
135 #[must_use]
136 pub(crate) fn is_present(&self) -> bool {
137 matches!(self, Self::Present(_))
138 }
139
140 #[cfg(test)]
141 #[must_use]
142 pub(crate) fn test_present_from_descriptor(
143 descriptor: kernel::DeletionVectorDescriptor,
144 ) -> Self {
145 Self::Present(KernelScanDeletionVectorHandle {
146 dv_info: descriptor.into(),
147 })
148 }
149}
150
151#[allow(dead_code)]
156#[derive(Clone)]
157pub(crate) enum KernelPhysicalToLogicalTransform {
158 NotRequired,
160 Required(KernelPhysicalToLogicalTransformHandle),
162}
163
164#[allow(dead_code)]
169#[derive(Clone)]
170pub(crate) struct KernelPhysicalToLogicalTransformHandle {
171 transform: kernel::ExpressionRef,
173}
174
175pub type DeltaStorageOptions = BTreeMap<String, String>;
177
178pub struct DeltaSourceConfig {
180 pub name: String,
182 pub table_uri: String,
184 pub version: Option<Version>,
186 pub storage_options: DeltaStorageOptions,
188}
189
190impl DeltaSourceConfig {
191 #[must_use]
193 pub fn new(name: impl Into<String>, table_uri: impl Into<String>) -> Self {
194 Self {
195 name: name.into(),
196 table_uri: table_uri.into(),
197 version: None,
198 storage_options: DeltaStorageOptions::default(),
199 }
200 }
201
202 #[must_use]
204 pub fn with_version(mut self, version: Option<Version>) -> Self {
205 self.version = version;
206 self
207 }
208
209 #[must_use]
211 pub fn with_storage_options(mut self, storage_options: DeltaStorageOptions) -> Self {
212 self.storage_options = storage_options;
213 self
214 }
215}
216
217pub struct PlannedDeltaSource {
219 name: String,
220 requested_table_uri: String,
221 storage_options: DeltaStorageOptions,
222 snapshot: LoadedDeltaTableSnapshot,
223}
224
225#[allow(dead_code)]
227pub(crate) struct ProjectedDeltaScan {
228 scan: kernel::Scan,
229 kernel_schema: kernel::KernelSchemaRef,
230}
231
232impl ProjectedDeltaScan {
233 #[allow(dead_code)]
235 #[must_use]
236 pub(crate) fn kernel_schema(&self) -> &kernel::KernelSchemaRef {
237 &self.kernel_schema
238 }
239
240 #[allow(dead_code)]
242 #[must_use]
243 pub(crate) fn kernel_scan(&self) -> &kernel::Scan {
244 &self.scan
245 }
246
247 #[allow(dead_code)]
249 #[must_use]
250 pub(crate) fn read_schema(&self) -> KernelScanReadSchema {
251 KernelScanReadSchema::new(
252 self.scan.physical_schema().clone(),
253 self.scan.logical_schema().clone(),
254 self.scan.physical_predicate().clone(),
255 )
256 }
257
258 #[allow(dead_code)]
264 pub(crate) fn expand_kernel_scan_metadata(
265 &self,
266 table_uri: &str,
267 storage_options: &DeltaStorageOptions,
268 ) -> Result<KernelScanMetadataExpansion, delta_kernel::Error> {
269 fn collect_scan_file(files: &mut Vec<KernelScanFileMetadata>, file: kernel::ScanFile) {
270 files.push(KernelScanFileMetadata::from_kernel_scan_file(file));
271 }
272
273 let table_url = kernel::try_parse_uri(table_uri)?;
274 let store = kernel::store_from_url_opts(
275 &table_url,
276 storage_options
277 .iter()
278 .map(|(key, value)| (key.as_str(), value.as_str())),
279 )?;
280 let engine = kernel::DefaultEngineBuilder::new(store).build();
281 let mut files = Vec::new();
282
283 for scan_metadata in self.scan.scan_metadata(&engine)? {
284 files = scan_metadata?.visit_scan_files(files, collect_scan_file)?;
285 }
286
287 Ok(KernelScanMetadataExpansion {
288 files,
289 scan_metadata_exhausted: true,
290 })
291 }
292
293 #[cfg(test)]
294 pub(crate) fn scan_file_paths(
296 &self,
297 table_uri: &str,
298 storage_options: &DeltaStorageOptions,
299 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
300 let mut paths = self
301 .expand_kernel_scan_metadata(table_uri, storage_options)?
302 .files
303 .into_iter()
304 .map(|file| file.path)
305 .collect::<Vec<_>>();
306 paths.sort();
307 Ok(paths)
308 }
309}
310
311impl KernelScanFileMetadata {
312 fn from_kernel_scan_file(file: kernel::ScanFile) -> Self {
313 let kernel::ScanFile {
314 path,
315 size,
316 modification_time,
317 stats,
318 dv_info,
319 transform,
320 partition_values,
321 } = file;
322
323 Self {
324 path,
325 size,
326 modification_time,
327 stats: stats.map(|stats| KernelScanFileStats {
328 num_records: stats.num_records,
329 }),
330 deletion_vector: KernelScanDeletionVectorMetadata::from_dv_info(dv_info),
331 physical_to_logical_transform: KernelPhysicalToLogicalTransform::from_transform(
332 transform,
333 ),
334 partition_values,
335 }
336 }
337}
338
339impl KernelScanDeletionVectorMetadata {
340 fn from_dv_info(dv_info: kernel::DvInfo) -> Self {
341 if dv_info.has_vector() {
342 Self::Present(KernelScanDeletionVectorHandle { dv_info })
343 } else {
344 Self::NotPresent
345 }
346 }
347}
348
349impl KernelPhysicalToLogicalTransform {
350 #[cfg(test)]
351 #[must_use]
352 pub(crate) fn is_required(&self) -> bool {
353 matches!(self, Self::Required(_))
354 }
355
356 #[cfg(test)]
357 pub(crate) fn test_required_column_transform(column_name: &str) -> Self {
358 let transform = std::sync::Arc::new(kernel::Expression::Column(kernel::ColumnName::new([
359 column_name,
360 ])));
361
362 Self::Required(KernelPhysicalToLogicalTransformHandle { transform })
363 }
364
365 #[cfg(test)]
366 pub(crate) fn test_required_orders_customer_name_replacement_transform(
367 replacement: &str,
368 ) -> Self {
369 let transform = std::sync::Arc::new(kernel::Expression::struct_from([
370 kernel::Expression::Column(kernel::ColumnName::new(["id"])),
371 kernel::Expression::Literal(kernel::Scalar::String(replacement.to_owned())),
372 ]));
373
374 Self::Required(KernelPhysicalToLogicalTransformHandle { transform })
375 }
376
377 fn from_transform(transform: Option<kernel::ExpressionRef>) -> Self {
378 match transform {
379 Some(transform) => Self::Required(KernelPhysicalToLogicalTransformHandle { transform }),
380 None => Self::NotRequired,
381 }
382 }
383}
384
385impl PlannedDeltaSource {
386 #[must_use]
388 pub fn name(&self) -> &str {
389 &self.name
390 }
391
392 #[must_use]
394 pub fn requested_table_uri(&self) -> &str {
395 &self.requested_table_uri
396 }
397
398 #[must_use]
400 pub fn storage_options(&self) -> &DeltaStorageOptions {
401 &self.storage_options
402 }
403
404 #[must_use]
406 pub fn table_uri(&self) -> &str {
407 self.loaded_snapshot().table_uri()
408 }
409
410 #[must_use]
412 pub fn version(&self) -> Version {
413 self.loaded_snapshot().version()
414 }
415
416 pub(crate) fn loaded_snapshot(&self) -> &LoadedDeltaTableSnapshot {
417 &self.snapshot
418 }
419}
420
421pub(crate) fn delta_source_arrow_schema(
422 source: &PlannedDeltaSource,
423) -> Result<ArrowSchemaRef, DeltaFunnelError> {
424 match snapshot_arrow_schema(source.loaded_snapshot().kernel_snapshot()) {
425 Ok(schema) => Ok(schema),
426 Err(error) => DeltaSourceSchemaSnafu {
427 source_name: source.name().to_owned(),
428 table_uri: source.table_uri().to_owned(),
429 reason: error.to_string(),
430 }
431 .fail(),
432 }
433}
434
435#[allow(dead_code)]
437pub(crate) fn build_projected_delta_scan(
438 source: &PlannedDeltaSource,
439 projected_column_names: Option<&[String]>,
440) -> Result<ProjectedDeltaScan, delta_kernel::Error> {
441 build_projected_predicated_delta_scan(source, projected_column_names, None)
442}
443
444#[allow(dead_code)]
450pub(crate) fn build_projected_predicated_delta_scan(
451 source: &PlannedDeltaSource,
452 projected_column_names: Option<&[String]>,
453 predicate: Option<DeltaKernelPredicate>,
454) -> Result<ProjectedDeltaScan, delta_kernel::Error> {
455 let (scan, kernel_schema) = kernel::build_projected_predicated_scan(
456 source.loaded_snapshot().kernel_snapshot(),
457 projected_column_names,
458 predicate,
459 )?;
460
461 Ok(ProjectedDeltaScan {
462 scan,
463 kernel_schema,
464 })
465}
466
467#[allow(dead_code)]
469pub(crate) fn build_projected_predicated_stats_delta_scan(
470 source: &PlannedDeltaSource,
471 projected_column_names: Option<&[String]>,
472 predicate: Option<DeltaKernelPredicate>,
473) -> Result<ProjectedDeltaScan, delta_kernel::Error> {
474 let (scan, kernel_schema) = kernel::build_projected_predicated_stats_scan(
475 source.loaded_snapshot().kernel_snapshot(),
476 projected_column_names,
477 predicate,
478 )?;
479
480 Ok(ProjectedDeltaScan {
481 scan,
482 kernel_schema,
483 })
484}
485
486pub fn load_delta_source(
499 config: DeltaSourceConfig,
500) -> Result<PlannedDeltaSource, DeltaFunnelError> {
501 validate_table_source_names([config.name.as_str()])?;
502
503 load_delta_source_after_name_validation(config, false)
504}
505
506pub(crate) fn load_delta_source_with_tracing(
507 config: DeltaSourceConfig,
508) -> Result<PlannedDeltaSource, DeltaFunnelError> {
509 validate_table_source_names([config.name.as_str()])?;
510
511 load_delta_source_after_name_validation(config, true)
512}
513
514pub fn load_delta_sources<I>(configs: I) -> Result<Vec<PlannedDeltaSource>, DeltaFunnelError>
526where
527 I: IntoIterator<Item = DeltaSourceConfig>,
528{
529 let configs: Vec<_> = configs.into_iter().collect();
530
531 validate_table_source_names(configs.iter().map(|config| config.name.as_str()))?;
532
533 configs
534 .into_iter()
535 .map(|config| load_delta_source_after_name_validation(config, false))
536 .collect()
537}
538
539fn load_delta_source_after_name_validation(
540 mut config: DeltaSourceConfig,
541 emit_tracing: bool,
542) -> Result<PlannedDeltaSource, DeltaFunnelError> {
543 config.storage_options = effective_storage_options_for_source_from_env(
544 &config.table_uri,
545 config.storage_options,
546 std::env::vars(),
547 );
548 let source_name = config.name.clone();
549 let s3_auth_mode_hint =
550 s3_auth_mode_hint_for_source(&config.table_uri, &config.storage_options);
551 if emit_tracing {
552 observability::source_loading_started(
553 &source_name,
554 s3_auth_mode_hint.map(|hint| hint.as_str()),
555 );
556 }
557
558 let DeltaSourceConfig {
559 name,
560 table_uri,
561 version,
562 storage_options,
563 } = config;
564
565 let result = load_delta_table_snapshot(&table_uri, version, &storage_options).map(|snapshot| {
566 PlannedDeltaSource {
567 name,
568 requested_table_uri: table_uri,
569 storage_options,
570 snapshot,
571 }
572 });
573
574 if emit_tracing {
575 match &result {
576 Ok(source) => observability::source_loading_completed(
577 source.name(),
578 source.version(),
579 s3_auth_mode_hint.map(|hint| hint.as_str()),
580 ),
581 Err(error) => observability::source_loading_failed(
582 &source_name,
583 error,
584 s3_auth_mode_hint.map(|hint| hint.as_str()),
585 ),
586 }
587 }
588
589 result
590}
591
592fn effective_storage_options_for_source_from_env<I>(
593 table_uri: &str,
594 storage_options: DeltaStorageOptions,
595 env: I,
596) -> DeltaStorageOptions
597where
598 I: IntoIterator<Item = (String, String)>,
599{
600 if s3_auth_mode_hint_for_source(table_uri, &storage_options).is_none() {
601 return storage_options;
602 }
603
604 let caller_s3_keys = storage_options
605 .keys()
606 .filter_map(|key| s3_option_precedence_key(key))
607 .collect::<HashSet<_>>();
608 let mut effective = env
609 .into_iter()
610 .filter(|(key, _)| key.starts_with("AWS_"))
611 .filter(|(key, _)| match s3_option_precedence_key(key) {
612 Some(key) => !caller_s3_keys.contains(&key),
613 None => true,
614 })
615 .collect::<DeltaStorageOptions>();
616 effective.extend(storage_options);
617 effective
618}
619
620fn s3_option_precedence_key(key: &str) -> Option<String> {
621 let key = key.to_ascii_lowercase().parse::<AmazonS3ConfigKey>().ok()?;
622 Some(match key {
623 AmazonS3ConfigKey::DefaultRegion | AmazonS3ConfigKey::Region => "aws_region".to_owned(),
624 _ => key.as_ref().to_owned(),
625 })
626}
627
628#[cfg(test)]
629mod tests {
630 use std::collections::{BTreeMap, HashMap};
631 use std::fs;
632 use std::path::{Path, PathBuf};
633 use std::sync::{Arc, Mutex};
634 use std::time::{SystemTime, UNIX_EPOCH};
635
636 use datafusion::common::ScalarValue;
637 use datafusion::logical_expr::{Expr, col, lit};
638 use delta_kernel::arrow::array::{Array, StructArray};
639 use delta_kernel::arrow::record_batch::RecordBatch;
640 use delta_kernel::object_store::{
641 aws::{AmazonS3Builder, AmazonS3ConfigKey},
642 memory::InMemory,
643 path::Path as ObjectStorePath,
644 };
645
646 use super::kernel::{ColumnName, Expression, Predicate, Scalar};
647 use super::{
648 DeltaKernelPredicate, DeltaSourceConfig, DeltaStorageOptions, KernelDataFileReader,
649 KernelDataFileReaderConfig, KernelDeletionVectorReader, KernelDeletionVectorReaderConfig,
650 ProjectedDeltaScan, build_projected_delta_scan, build_projected_predicated_delta_scan,
651 datafusion_expr_to_kernel_predicate, effective_storage_options_for_source_from_env,
652 load_delta_source, load_delta_sources, load_delta_table_snapshot,
653 };
654 use crate::DeltaFunnelError;
655
656 struct DeltaLogTable {
657 path: PathBuf,
658 }
659
660 impl Drop for DeltaLogTable {
661 fn drop(&mut self) {
662 let _ = fs::remove_dir_all(&self.path);
663 }
664 }
665
666 impl DeltaLogTable {
667 fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
668 Self::new_with_metadata_and_adds(name, METADATA_JSON, &[add_json("part-00001.parquet")])
669 }
670
671 fn new_with_metadata_and_adds(
672 name: &str,
673 metadata_json: &str,
674 add_jsons: &[String],
675 ) -> Result<Self, Box<dyn std::error::Error>> {
676 Self::new_with_protocol_metadata_and_adds(name, PROTOCOL_JSON, metadata_json, add_jsons)
677 }
678
679 fn new_with_protocol_metadata_and_adds(
680 name: &str,
681 protocol_json: &str,
682 metadata_json: &str,
683 add_jsons: &[String],
684 ) -> Result<Self, Box<dyn std::error::Error>> {
685 let path = Path::new("target")
686 .join("delta-funnel-named-source-tests")
687 .join(unique_name(name)?);
688 let log_path = path.join("_delta_log");
689 fs::create_dir_all(&log_path)?;
690 fs::write(
691 log_path.join("00000000000000000000.json"),
692 format!("{protocol_json}\n{metadata_json}\n"),
693 )?;
694 fs::write(log_path.join("00000000000000000001.json"), {
695 let mut actions = add_jsons.join("\n");
696 actions.push('\n');
697 actions
698 })?;
699
700 Ok(Self { path })
701 }
702 }
703
704 const PROTOCOL_JSON: &str = r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}"#;
705 const METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
706 const BOOLEAN_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"is_current\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
707 const BINARY_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"payload\",\"type\":\"binary\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
708 const DATE_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"event_date\",\"type\":\"date\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
709 const DECIMAL_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"amount\",\"type\":\"decimal(10,2)\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
710 const FLOATING_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"float_score\",\"type\":\"float\",\"nullable\":true,\"metadata\":{}},{\"name\":\"double_score\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
711 const STRING_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"customer_name\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
712 const TIMESTAMP_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"event_ts\",\"type\":\"timestamp\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
713 const TIMESTAMP_NTZ_DATA_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"event_ts_ntz\",\"type\":\"timestamp_ntz\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}"#;
714 const PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"region\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["region"],"configuration":{},"createdTime":1587968585495}}"#;
715 const INTEGER_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"byte_part\",\"type\":\"byte\",\"nullable\":true,\"metadata\":{}},{\"name\":\"short_part\",\"type\":\"short\",\"nullable\":true,\"metadata\":{}},{\"name\":\"int_part\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"long_part\",\"type\":\"long\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["byte_part","short_part","int_part","long_part"],"configuration":{},"createdTime":1587968585495}}"#;
716 const BOOLEAN_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"is_current\",\"type\":\"boolean\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["is_current"],"configuration":{},"createdTime":1587968585495}}"#;
717 const DATE_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"event_date\",\"type\":\"date\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["event_date"],"configuration":{},"createdTime":1587968585495}}"#;
718 const DECIMAL_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"amount\",\"type\":\"decimal(10,2)\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["amount"],"configuration":{},"createdTime":1587968585495}}"#;
719 const FLOATING_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"float_part\",\"type\":\"float\",\"nullable\":true,\"metadata\":{}},{\"name\":\"double_part\",\"type\":\"double\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["float_part","double_part"],"configuration":{},"createdTime":1587968585495}}"#;
720 const BINARY_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"payload\",\"type\":\"binary\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["payload"],"configuration":{},"createdTime":1587968585495}}"#;
721 const TIMESTAMP_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"event_ts\",\"type\":\"timestamp\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["event_ts"],"configuration":{},"createdTime":1587968585495}}"#;
722 const TIMESTAMP_NTZ_PROTOCOL_JSON: &str = r#"{"protocol":{"minReaderVersion":3,"minWriterVersion":7,"readerFeatures":["timestampNtz"],"writerFeatures":["timestampNtz"]}}"#;
723 const TIMESTAMP_NTZ_PARTITIONED_METADATA_JSON: &str = r#"{"metaData":{"id":"delta-funnel-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"event_ts_ntz\",\"type\":\"timestamp_ntz\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["event_ts_ntz"],"configuration":{},"createdTime":1587968585495}}"#;
724 const DELETION_VECTOR_PROTOCOL_JSON: &str = r#"{"protocol":{"minReaderVersion":3,"minWriterVersion":7,"readerFeatures":["deletionVectors"],"writerFeatures":["deletionVectors"]}}"#;
725
726 fn add_json(path: &str) -> String {
727 format!(
728 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true}}}}"#
729 )
730 }
731
732 fn partitioned_add_json(path: &str, partition_values_json: &str) -> String {
733 format!(
734 r#"{{"add":{{"path":"{path}","partitionValues":{partition_values_json},"size":0,"modificationTime":1587968586000,"dataChange":true}}}}"#
735 )
736 }
737
738 fn add_json_with_id_stats(
739 path: &str,
740 num_records: i64,
741 min_value: i32,
742 max_value: i32,
743 null_count: i64,
744 ) -> String {
745 format!(
746 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"id\":{min_value}}},\"maxValues\":{{\"id\":{max_value}}},\"nullCount\":{{\"id\":{null_count}}}}}"}}}}"#
747 )
748 }
749
750 fn add_json_with_boolean_stats(
751 path: &str,
752 num_records: i64,
753 min_value: bool,
754 max_value: bool,
755 null_count: i64,
756 ) -> String {
757 format!(
758 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"is_current\":{min_value}}},\"maxValues\":{{\"is_current\":{max_value}}},\"nullCount\":{{\"is_current\":{null_count}}}}}"}}}}"#
759 )
760 }
761
762 fn add_json_with_partial_boolean_stats(
763 path: &str,
764 num_records: i64,
765 min_value: Option<bool>,
766 max_value: Option<bool>,
767 null_count: Option<i64>,
768 ) -> String {
769 let min_values = min_value
770 .map(|value| format!(r#"\"is_current\":{value}"#))
771 .unwrap_or_default();
772 let max_values = max_value
773 .map(|value| format!(r#"\"is_current\":{value}"#))
774 .unwrap_or_default();
775 let null_count = null_count
776 .map(|value| format!(r#"\"is_current\":{value}"#))
777 .unwrap_or_default();
778
779 format!(
780 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
781 )
782 }
783
784 fn add_json_with_date_stats(
785 path: &str,
786 num_records: i64,
787 min_value: &str,
788 max_value: &str,
789 null_count: i64,
790 ) -> String {
791 format!(
792 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"event_date\":\"{min_value}\"}},\"maxValues\":{{\"event_date\":\"{max_value}\"}},\"nullCount\":{{\"event_date\":{null_count}}}}}"}}}}"#
793 )
794 }
795
796 fn add_json_with_partial_date_stats(
797 path: &str,
798 num_records: i64,
799 min_value: Option<&str>,
800 max_value: Option<&str>,
801 null_count: Option<i64>,
802 ) -> String {
803 let min_values = min_value
804 .map(|value| format!(r#"\"event_date\":\"{value}\""#))
805 .unwrap_or_default();
806 let max_values = max_value
807 .map(|value| format!(r#"\"event_date\":\"{value}\""#))
808 .unwrap_or_default();
809 let null_count = null_count
810 .map(|value| format!(r#"\"event_date\":{value}"#))
811 .unwrap_or_default();
812
813 format!(
814 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
815 )
816 }
817
818 fn add_json_with_decimal_stats(
819 path: &str,
820 num_records: i64,
821 min_value: &str,
822 max_value: &str,
823 null_count: i64,
824 ) -> String {
825 format!(
826 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"amount\":\"{min_value}\"}},\"maxValues\":{{\"amount\":\"{max_value}\"}},\"nullCount\":{{\"amount\":{null_count}}}}}"}}}}"#
827 )
828 }
829
830 fn add_json_with_partial_decimal_stats(
831 path: &str,
832 num_records: i64,
833 min_value: Option<&str>,
834 max_value: Option<&str>,
835 null_count: Option<i64>,
836 ) -> String {
837 let min_values = min_value
838 .map(|value| format!(r#"\"amount\":\"{value}\""#))
839 .unwrap_or_default();
840 let max_values = max_value
841 .map(|value| format!(r#"\"amount\":\"{value}\""#))
842 .unwrap_or_default();
843 let null_count = null_count
844 .map(|value| format!(r#"\"amount\":{value}"#))
845 .unwrap_or_default();
846
847 format!(
848 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
849 )
850 }
851
852 fn add_json_with_binary_stats(
853 path: &str,
854 num_records: i64,
855 min_value: &str,
856 max_value: &str,
857 null_count: i64,
858 ) -> String {
859 format!(
860 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"payload\":\"{min_value}\"}},\"maxValues\":{{\"payload\":\"{max_value}\"}},\"nullCount\":{{\"payload\":{null_count}}}}}"}}}}"#
861 )
862 }
863
864 fn add_json_with_partial_binary_stats(
865 path: &str,
866 num_records: i64,
867 min_value: Option<&str>,
868 max_value: Option<&str>,
869 null_count: Option<i64>,
870 ) -> String {
871 let min_values = min_value
872 .map(|value| format!(r#"\"payload\":\"{value}\""#))
873 .unwrap_or_default();
874 let max_values = max_value
875 .map(|value| format!(r#"\"payload\":\"{value}\""#))
876 .unwrap_or_default();
877 let null_count = null_count
878 .map(|value| format!(r#"\"payload\":{value}"#))
879 .unwrap_or_default();
880
881 format!(
882 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
883 )
884 }
885
886 fn add_json_with_floating_stats(
887 path: &str,
888 num_records: i64,
889 float_min_value: &str,
890 float_max_value: &str,
891 double_min_value: &str,
892 double_max_value: &str,
893 null_count: i64,
894 ) -> String {
895 format!(
896 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"float_score\":{float_min_value},\"double_score\":{double_min_value}}},\"maxValues\":{{\"float_score\":{float_max_value},\"double_score\":{double_max_value}}},\"nullCount\":{{\"float_score\":{null_count},\"double_score\":{null_count}}}}}"}}}}"#
897 )
898 }
899
900 fn add_json_with_partial_floating_stats(
901 path: &str,
902 num_records: i64,
903 float_min_value: Option<&str>,
904 float_max_value: Option<&str>,
905 double_min_value: Option<&str>,
906 double_max_value: Option<&str>,
907 null_count: Option<i64>,
908 ) -> String {
909 let mut min_values = Vec::new();
910 if let Some(value) = float_min_value {
911 min_values.push(format!(r#"\"float_score\":{value}"#));
912 }
913 if let Some(value) = double_min_value {
914 min_values.push(format!(r#"\"double_score\":{value}"#));
915 }
916
917 let mut max_values = Vec::new();
918 if let Some(value) = float_max_value {
919 max_values.push(format!(r#"\"float_score\":{value}"#));
920 }
921 if let Some(value) = double_max_value {
922 max_values.push(format!(r#"\"double_score\":{value}"#));
923 }
924
925 let null_count = null_count
926 .map(|value| format!(r#"\"float_score\":{value},\"double_score\":{value}"#))
927 .unwrap_or_default();
928
929 format!(
930 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{}}},\"maxValues\":{{{}}},\"nullCount\":{{{null_count}}}}}"}}}}"#,
931 min_values.join(","),
932 max_values.join(",")
933 )
934 }
935
936 fn add_json_with_string_stats(
937 path: &str,
938 num_records: i64,
939 min_value: &str,
940 max_value: &str,
941 null_count: i64,
942 ) -> String {
943 format!(
944 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"customer_name\":\"{min_value}\"}},\"maxValues\":{{\"customer_name\":\"{max_value}\"}},\"nullCount\":{{\"customer_name\":{null_count}}}}}"}}}}"#
945 )
946 }
947
948 fn add_json_with_partial_string_stats(
949 path: &str,
950 num_records: i64,
951 min_value: Option<&str>,
952 max_value: Option<&str>,
953 null_count: Option<i64>,
954 ) -> String {
955 let min_values = min_value
956 .map(|value| format!(r#"\"customer_name\":\"{value}\""#))
957 .unwrap_or_default();
958 let max_values = max_value
959 .map(|value| format!(r#"\"customer_name\":\"{value}\""#))
960 .unwrap_or_default();
961 let null_count = null_count
962 .map(|value| format!(r#"\"customer_name\":{value}"#))
963 .unwrap_or_default();
964
965 format!(
966 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
967 )
968 }
969
970 fn add_json_with_timestamp_stats(
971 path: &str,
972 column_name: &str,
973 num_records: i64,
974 min_value: &str,
975 max_value: &str,
976 null_count: i64,
977 ) -> String {
978 format!(
979 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"{column_name}\":\"{min_value}\"}},\"maxValues\":{{\"{column_name}\":\"{max_value}\"}},\"nullCount\":{{\"{column_name}\":{null_count}}}}}"}}}}"#
980 )
981 }
982
983 fn add_json_with_partial_timestamp_stats(
984 path: &str,
985 column_name: &str,
986 num_records: i64,
987 min_value: Option<&str>,
988 max_value: Option<&str>,
989 null_count: Option<i64>,
990 ) -> String {
991 let min_values = min_value
992 .map(|value| format!(r#"\"{column_name}\":\"{value}\""#))
993 .unwrap_or_default();
994 let max_values = max_value
995 .map(|value| format!(r#"\"{column_name}\":\"{value}\""#))
996 .unwrap_or_default();
997 let null_count = null_count
998 .map(|value| format!(r#"\"{column_name}\":{value}"#))
999 .unwrap_or_default();
1000
1001 format!(
1002 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{{min_values}}},\"maxValues\":{{{max_values}}},\"nullCount\":{{{null_count}}}}}"}}}}"#
1003 )
1004 }
1005
1006 fn dv_add_json_with_id_stats(
1007 path: &str,
1008 num_records: i64,
1009 min_value: i32,
1010 max_value: i32,
1011 null_count: i64,
1012 ) -> String {
1013 format!(
1014 r#"{{"add":{{"path":"{path}","partitionValues":{{}},"size":0,"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"id\":{min_value}}},\"maxValues\":{{\"id\":{max_value}}},\"nullCount\":{{\"id\":{null_count}}}}}","deletionVector":{{"storageType":"u","pathOrInlineDv":"vBn[lx{{q8@P<9BNH/isA","offset":1,"sizeInBytes":36,"cardinality":2}}}}}}"#
1015 )
1016 }
1017
1018 fn partitioned_dv_add_json(path: &str, partition_values_json: &str) -> String {
1019 format!(
1020 r#"{{"add":{{"path":"{path}","partitionValues":{partition_values_json},"size":0,"modificationTime":1587968586000,"dataChange":true,"deletionVector":{{"storageType":"u","pathOrInlineDv":"vBn[lx{{q8@P<9BNH/isA","offset":1,"sizeInBytes":36,"cardinality":2}}}}}}"#
1021 )
1022 }
1023
1024 fn partitioned_dv_add_json_with_id_stats_and_size(
1025 path: &str,
1026 partition_values_json: &str,
1027 size: i64,
1028 num_records: i64,
1029 min_value: i32,
1030 max_value: i32,
1031 null_count: i64,
1032 ) -> String {
1033 format!(
1034 r#"{{"add":{{"path":"{path}","partitionValues":{partition_values_json},"size":{size},"modificationTime":1587968586000,"dataChange":true,"stats":"{{\"numRecords\":{num_records},\"minValues\":{{\"id\":{min_value}}},\"maxValues\":{{\"id\":{max_value}}},\"nullCount\":{{\"id\":{null_count}}}}}","deletionVector":{{"storageType":"u","pathOrInlineDv":"vBn[lx{{q8@P<9BNH/isA","offset":1,"sizeInBytes":36,"cardinality":2}}}}}}"#
1035 )
1036 }
1037
1038 fn integer_partitioned_add_json(path: &str, value: &str) -> String {
1039 partitioned_add_json(
1040 path,
1041 &format!(
1042 r#"{{"byte_part":"{value}","short_part":"{value}","int_part":"{value}","long_part":"{value}"}}"#
1043 ),
1044 )
1045 }
1046
1047 fn unique_name(name: &str) -> Result<String, Box<dyn std::error::Error>> {
1048 let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
1049
1050 Ok(format!("{}-{}-{nanos}", std::process::id(), name))
1051 }
1052
1053 type CapturedStorageOptions = Arc<Mutex<Vec<DeltaStorageOptions>>>;
1054
1055 fn storage_options(entries: &[(&str, &str)]) -> DeltaStorageOptions {
1056 entries
1057 .iter()
1058 .map(|(key, value)| ((*key).to_owned(), (*value).to_owned()))
1059 .collect()
1060 }
1061
1062 fn env_vars(entries: &[(&str, &str)]) -> Vec<(String, String)> {
1063 entries
1064 .iter()
1065 .map(|(key, value)| ((*key).to_owned(), (*value).to_owned()))
1066 .collect()
1067 }
1068
1069 fn s3_builder_from_storage_options(options: &DeltaStorageOptions) -> AmazonS3Builder {
1070 options.iter().fold(
1071 AmazonS3Builder::new().with_url("s3://bucket/root"),
1072 |builder, (key, value)| match key.to_ascii_lowercase().parse::<AmazonS3ConfigKey>() {
1073 Ok(key) => builder.with_config(key, value.as_str()),
1074 Err(_) => builder,
1075 },
1076 )
1077 }
1078
1079 fn unique_storage_scheme(name: &str) -> Result<String, Box<dyn std::error::Error>> {
1080 let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
1081 let sanitized_name = name
1082 .chars()
1083 .filter(|character| character.is_ascii_alphanumeric())
1084 .collect::<String>();
1085
1086 Ok(format!("df{sanitized_name}{}{}", std::process::id(), nanos))
1087 }
1088
1089 fn register_capturing_storage_handler(
1090 scheme: &str,
1091 captured: CapturedStorageOptions,
1092 ) -> Result<(), Box<dyn std::error::Error>> {
1093 super::kernel::insert_url_handler(
1094 scheme,
1095 Arc::new(move |_url, options| {
1096 let options = options.into_iter().collect::<BTreeMap<_, _>>();
1097 captured
1098 .lock()
1099 .map_err(|_| delta_kernel::object_store::Error::Generic {
1100 store: "capture",
1101 source: std::io::Error::other("captured storage options lock poisoned")
1102 .into(),
1103 })?
1104 .push(options);
1105
1106 Ok((Box::new(InMemory::new()), ObjectStorePath::from("")))
1107 }),
1108 )?;
1109
1110 Ok(())
1111 }
1112
1113 fn register_failing_storage_handler(
1114 scheme: &str,
1115 captured: CapturedStorageOptions,
1116 secret_value: &'static str,
1117 ) -> Result<(), Box<dyn std::error::Error>> {
1118 super::kernel::insert_url_handler(
1119 scheme,
1120 Arc::new(move |_url, options| {
1121 let options = options.into_iter().collect::<BTreeMap<_, _>>();
1122 captured
1123 .lock()
1124 .map_err(|_| delta_kernel::object_store::Error::Generic {
1125 store: "capture",
1126 source: std::io::Error::other("captured storage options lock poisoned")
1127 .into(),
1128 })?
1129 .push(options);
1130
1131 Err(delta_kernel::object_store::Error::Generic {
1132 store: "capture",
1133 source: std::io::Error::other(secret_value).into(),
1134 })
1135 }),
1136 )?;
1137
1138 Ok(())
1139 }
1140
1141 fn captured_storage_options(captured: &CapturedStorageOptions) -> Vec<DeltaStorageOptions> {
1142 captured
1143 .lock()
1144 .map(|options| options.clone())
1145 .unwrap_or_default()
1146 }
1147
1148 fn kernel_scan_file_paths(
1149 scan: &ProjectedDeltaScan,
1150 table_uri: &str,
1151 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
1152 let storage_options = DeltaStorageOptions::default();
1153 let mut paths = scan
1154 .expand_kernel_scan_metadata(table_uri, &storage_options)?
1155 .files
1156 .into_iter()
1157 .map(|file| file.path)
1158 .collect::<Vec<_>>();
1159 paths.sort();
1160 Ok(paths)
1161 }
1162
1163 #[derive(Debug, PartialEq, Eq)]
1164 struct KernelScanFileBoundary {
1165 path: String,
1166 has_deletion_vector: bool,
1167 }
1168
1169 fn kernel_scan_file_boundaries(
1170 scan: &ProjectedDeltaScan,
1171 table_uri: &str,
1172 ) -> Result<Vec<KernelScanFileBoundary>, Box<dyn std::error::Error>> {
1173 let storage_options = DeltaStorageOptions::default();
1174 let mut boundaries = scan
1175 .expand_kernel_scan_metadata(table_uri, &storage_options)?
1176 .files
1177 .into_iter()
1178 .map(|file| KernelScanFileBoundary {
1179 path: file.path,
1180 has_deletion_vector: file.deletion_vector.is_present(),
1181 })
1182 .collect::<Vec<_>>();
1183 boundaries.sort_by(|left, right| left.path.cmp(&right.path));
1184 Ok(boundaries)
1185 }
1186
1187 #[test]
1188 fn kernel_scan_metadata_expansion_preserves_file_metadata_without_data_reads()
1189 -> Result<(), Box<dyn std::error::Error>> {
1190 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
1191 "kernel-scan-metadata-expansion",
1192 DELETION_VECTOR_PROTOCOL_JSON,
1193 PARTITIONED_METADATA_JSON,
1194 &[partitioned_dv_add_json_with_id_stats_and_size(
1195 "region-west-dv.parquet",
1196 r#"{"region":"us-west"}"#,
1197 123,
1198 7,
1199 10,
1200 20,
1201 0,
1202 )],
1203 )?;
1204 let source = load_delta_source(DeltaSourceConfig {
1205 name: "orders".to_owned(),
1206 table_uri: table.path.to_string_lossy().to_string(),
1207 version: None,
1208 storage_options: Default::default(),
1209 })?;
1210 let scan = build_projected_predicated_stats_delta_scan(&source, None)?;
1211
1212 let expansion =
1213 scan.expand_kernel_scan_metadata(source.table_uri(), source.storage_options())?;
1214
1215 assert!(expansion.scan_metadata_exhausted);
1216 assert_eq!(expansion.files.len(), 1);
1217 let file = &expansion.files[0];
1218 assert_eq!(file.path, "region-west-dv.parquet");
1219 assert_eq!(file.size, 123);
1220 assert_eq!(file.modification_time, 1587968586000);
1221 assert_eq!(file.stats.as_ref().map(|stats| stats.num_records), Some(7));
1222 assert!(file.deletion_vector.is_present());
1223 assert_eq!(
1224 file.partition_values.get("region").map(String::as_str),
1225 Some("us-west")
1226 );
1227
1228 Ok(())
1229 }
1230
1231 #[test]
1232 fn kernel_scan_metadata_expansion_reports_empty_scan_as_exhausted()
1233 -> Result<(), Box<dyn std::error::Error>> {
1234 let table = DeltaLogTable::new_with_metadata_and_adds(
1235 "kernel-scan-metadata-empty",
1236 METADATA_JSON,
1237 &[],
1238 )?;
1239 let source = load_delta_source(DeltaSourceConfig {
1240 name: "orders".to_owned(),
1241 table_uri: table.path.to_string_lossy().to_string(),
1242 version: None,
1243 storage_options: Default::default(),
1244 })?;
1245 let scan = build_projected_predicated_stats_delta_scan(&source, None)?;
1246
1247 let expansion =
1248 scan.expand_kernel_scan_metadata(source.table_uri(), source.storage_options())?;
1249
1250 assert!(expansion.scan_metadata_exhausted);
1251 assert!(expansion.files.is_empty());
1252
1253 Ok(())
1254 }
1255
1256 #[test]
1257 fn kernel_scan_file_metadata_preserves_transform_handle_without_execution()
1258 -> Result<(), Box<dyn std::error::Error>> {
1259 let transform = Arc::new(Expression::Column(ColumnName::new(["physical_id"])));
1260 let partition_values = HashMap::from([("region".to_owned(), "us-west".to_owned())]);
1261
1262 let file = super::kernel::ScanFile {
1263 path: "part-with-transform.parquet".to_owned(),
1264 size: 456,
1265 modification_time: 1587968587000,
1266 stats: None,
1267 dv_info: super::kernel::DvInfo::default(),
1268 transform: Some(Arc::clone(&transform)),
1269 partition_values,
1270 };
1271
1272 let metadata = super::KernelScanFileMetadata::from_kernel_scan_file(file);
1273
1274 assert_eq!(metadata.path, "part-with-transform.parquet");
1275 assert_eq!(metadata.size, 456);
1276 assert_eq!(metadata.modification_time, 1587968587000);
1277 assert!(metadata.stats.is_none());
1278 assert!(!metadata.deletion_vector.is_present());
1279 assert!(metadata.physical_to_logical_transform.is_required());
1280 assert_eq!(
1281 metadata.partition_values.get("region").map(String::as_str),
1282 Some("us-west")
1283 );
1284 match metadata.physical_to_logical_transform {
1285 super::KernelPhysicalToLogicalTransform::Required(handle) => {
1286 assert!(Arc::ptr_eq(&handle.transform, &transform));
1287 }
1288 super::KernelPhysicalToLogicalTransform::NotRequired => {
1289 return Err("transform handle should be preserved".into());
1290 }
1291 }
1292
1293 Ok(())
1294 }
1295
1296 fn kernel_partition_characterization_source()
1297 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1298 let table = DeltaLogTable::new_with_metadata_and_adds(
1299 "kernel-partition-characterization",
1300 PARTITIONED_METADATA_JSON,
1301 &[
1302 partitioned_add_json("region-us-west.parquet", r#"{"region":"us-west"}"#),
1303 partitioned_add_json("region-us-east.parquet", r#"{"region":"us-east"}"#),
1304 partitioned_add_json("region-null.parquet", r#"{"region":null}"#),
1305 partitioned_add_json("region-missing.parquet", r#"{}"#),
1306 partitioned_add_json("region-empty-string.parquet", r#"{"region":""}"#),
1307 ],
1308 )?;
1309 let source = load_delta_source(DeltaSourceConfig {
1310 name: "orders".to_owned(),
1311 table_uri: table.path.to_string_lossy().to_string(),
1312 version: None,
1313 storage_options: Default::default(),
1314 })?;
1315
1316 Ok((table, source))
1317 }
1318
1319 fn kernel_integer_partition_characterization_source()
1320 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1321 let table = DeltaLogTable::new_with_metadata_and_adds(
1322 "kernel-integer-partition-characterization",
1323 INTEGER_PARTITIONED_METADATA_JSON,
1324 &[
1325 integer_partitioned_add_json("integer--1.parquet", "-1"),
1326 integer_partitioned_add_json("integer-2.parquet", "2"),
1327 integer_partitioned_add_json("integer-10.parquet", "10"),
1328 partitioned_add_json(
1329 "integer-null.parquet",
1330 r#"{"byte_part":null,"short_part":null,"int_part":null,"long_part":null}"#,
1331 ),
1332 integer_partitioned_add_json("integer-empty.parquet", ""),
1333 partitioned_add_json("integer-missing.parquet", r#"{}"#),
1334 ],
1335 )?;
1336 let source = load_delta_source(DeltaSourceConfig {
1337 name: "orders".to_owned(),
1338 table_uri: table.path.to_string_lossy().to_string(),
1339 version: None,
1340 storage_options: Default::default(),
1341 })?;
1342
1343 Ok((table, source))
1344 }
1345
1346 fn kernel_boolean_partition_characterization_source()
1347 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1348 let table = DeltaLogTable::new_with_metadata_and_adds(
1349 "kernel-boolean-partition-characterization",
1350 BOOLEAN_PARTITIONED_METADATA_JSON,
1351 &[
1352 partitioned_add_json("boolean-true.parquet", r#"{"is_current":"true"}"#),
1353 partitioned_add_json("boolean-false.parquet", r#"{"is_current":"false"}"#),
1354 partitioned_add_json("boolean-null.parquet", r#"{"is_current":null}"#),
1355 partitioned_add_json("boolean-empty.parquet", r#"{"is_current":""}"#),
1356 partitioned_add_json("boolean-missing.parquet", r#"{}"#),
1357 ],
1358 )?;
1359 let source = load_delta_source(DeltaSourceConfig {
1360 name: "orders".to_owned(),
1361 table_uri: table.path.to_string_lossy().to_string(),
1362 version: None,
1363 storage_options: Default::default(),
1364 })?;
1365
1366 Ok((table, source))
1367 }
1368
1369 fn kernel_date_partition_characterization_source()
1370 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1371 let table = DeltaLogTable::new_with_metadata_and_adds(
1372 "kernel-date-partition-characterization",
1373 DATE_PARTITIONED_METADATA_JSON,
1374 &[
1375 partitioned_add_json("date-pre-epoch.parquet", r#"{"event_date":"1969-12-31"}"#),
1376 partitioned_add_json("date-epoch.parquet", r#"{"event_date":"1970-01-01"}"#),
1377 partitioned_add_json("date-leap-day.parquet", r#"{"event_date":"2024-02-29"}"#),
1378 partitioned_add_json("date-new-year.parquet", r#"{"event_date":"2026-01-01"}"#),
1379 partitioned_add_json("date-null.parquet", r#"{"event_date":null}"#),
1380 partitioned_add_json("date-empty.parquet", r#"{"event_date":""}"#),
1381 partitioned_add_json("date-missing.parquet", r#"{}"#),
1382 ],
1383 )?;
1384 let source = load_delta_source(DeltaSourceConfig {
1385 name: "orders".to_owned(),
1386 table_uri: table.path.to_string_lossy().to_string(),
1387 version: None,
1388 storage_options: Default::default(),
1389 })?;
1390
1391 Ok((table, source))
1392 }
1393
1394 fn kernel_decimal_partition_characterization_source()
1395 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1396 let table = DeltaLogTable::new_with_metadata_and_adds(
1397 "kernel-decimal-partition-characterization",
1398 DECIMAL_PARTITIONED_METADATA_JSON,
1399 &[
1400 partitioned_add_json("decimal-negative.parquet", r#"{"amount":"-1.23"}"#),
1401 partitioned_add_json("decimal-zero.parquet", r#"{"amount":"0.00"}"#),
1402 partitioned_add_json("decimal-two.parquet", r#"{"amount":"2.00"}"#),
1403 partitioned_add_json("decimal-ten.parquet", r#"{"amount":"10.00"}"#),
1404 partitioned_add_json("decimal-large.parquet", r#"{"amount":"123.45"}"#),
1405 partitioned_add_json("decimal-null.parquet", r#"{"amount":null}"#),
1406 partitioned_add_json("decimal-empty.parquet", r#"{"amount":""}"#),
1407 partitioned_add_json("decimal-missing.parquet", r#"{}"#),
1408 ],
1409 )?;
1410 let source = load_delta_source(DeltaSourceConfig {
1411 name: "orders".to_owned(),
1412 table_uri: table.path.to_string_lossy().to_string(),
1413 version: None,
1414 storage_options: Default::default(),
1415 })?;
1416
1417 Ok((table, source))
1418 }
1419
1420 fn kernel_floating_partition_characterization_source()
1421 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1422 let table = DeltaLogTable::new_with_metadata_and_adds(
1423 "kernel-floating-partition-characterization",
1424 FLOATING_PARTITIONED_METADATA_JSON,
1425 &[
1426 partitioned_add_json(
1427 "floating-neg.parquet",
1428 r#"{"float_part":"-1.5","double_part":"-2.25"}"#,
1429 ),
1430 partitioned_add_json(
1431 "floating-neg-zero.parquet",
1432 r#"{"float_part":"-0.0","double_part":"-0.0"}"#,
1433 ),
1434 partitioned_add_json(
1435 "floating-pos-zero.parquet",
1436 r#"{"float_part":"0.0","double_part":"0.0"}"#,
1437 ),
1438 partitioned_add_json(
1439 "floating-one.parquet",
1440 r#"{"float_part":"1.5","double_part":"2.25"}"#,
1441 ),
1442 partitioned_add_json(
1443 "floating-ten.parquet",
1444 r#"{"float_part":"10.0","double_part":"10.0"}"#,
1445 ),
1446 partitioned_add_json(
1447 "floating-null.parquet",
1448 r#"{"float_part":null,"double_part":null}"#,
1449 ),
1450 partitioned_add_json(
1451 "floating-empty.parquet",
1452 r#"{"float_part":"","double_part":""}"#,
1453 ),
1454 partitioned_add_json("floating-missing.parquet", r#"{}"#),
1455 ],
1456 )?;
1457 let source = load_delta_source(DeltaSourceConfig {
1458 name: "orders".to_owned(),
1459 table_uri: table.path.to_string_lossy().to_string(),
1460 version: None,
1461 storage_options: Default::default(),
1462 })?;
1463
1464 Ok((table, source))
1465 }
1466
1467 fn kernel_binary_partition_characterization_source()
1468 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1469 let table = DeltaLogTable::new_with_metadata_and_adds(
1470 "kernel-binary-partition-characterization",
1471 BINARY_PARTITIONED_METADATA_JSON,
1472 &[
1473 partitioned_add_json("binary-HELLO.parquet", r#"{"payload":"HELLO"}"#),
1474 partitioned_add_json("binary-hello.parquet", r#"{"payload":"hello"}"#),
1475 partitioned_add_json("binary-special.parquet", r#"{"payload":"/=%"}"#),
1476 partitioned_add_json("binary-null.parquet", r#"{"payload":null}"#),
1477 partitioned_add_json("binary-empty.parquet", r#"{"payload":""}"#),
1478 partitioned_add_json("binary-missing.parquet", r#"{}"#),
1479 ],
1480 )?;
1481 let source = load_delta_source(DeltaSourceConfig {
1482 name: "orders".to_owned(),
1483 table_uri: table.path.to_string_lossy().to_string(),
1484 version: None,
1485 storage_options: Default::default(),
1486 })?;
1487
1488 Ok((table, source))
1489 }
1490
1491 fn kernel_timestamp_partition_characterization_source()
1492 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1493 let table = DeltaLogTable::new_with_metadata_and_adds(
1494 "kernel-timestamp-partition-characterization",
1495 TIMESTAMP_PARTITIONED_METADATA_JSON,
1496 &[
1497 partitioned_add_json(
1498 "timestamp-pre-epoch.parquet",
1499 r#"{"event_ts":"1969-12-31T23:59:59.999999Z"}"#,
1500 ),
1501 partitioned_add_json(
1502 "timestamp-low.parquet",
1503 r#"{"event_ts":"2025-12-31T23:59:59.999999Z"}"#,
1504 ),
1505 partitioned_add_json(
1506 "timestamp-target.parquet",
1507 r#"{"event_ts":"2026-01-01T00:00:00.123456Z"}"#,
1508 ),
1509 partitioned_add_json(
1510 "timestamp-high.parquet",
1511 r#"{"event_ts":"2026-01-01T00:00:00.123457Z"}"#,
1512 ),
1513 partitioned_add_json("timestamp-null.parquet", r#"{"event_ts":null}"#),
1514 partitioned_add_json("timestamp-empty.parquet", r#"{"event_ts":""}"#),
1515 partitioned_add_json("timestamp-missing.parquet", r#"{}"#),
1516 ],
1517 )?;
1518 let source = load_delta_source(DeltaSourceConfig {
1519 name: "orders".to_owned(),
1520 table_uri: table.path.to_string_lossy().to_string(),
1521 version: None,
1522 storage_options: Default::default(),
1523 })?;
1524
1525 Ok((table, source))
1526 }
1527
1528 fn kernel_timestamp_ntz_partition_characterization_source()
1529 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1530 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
1531 "kernel-timestamp-ntz-partition-characterization",
1532 TIMESTAMP_NTZ_PROTOCOL_JSON,
1533 TIMESTAMP_NTZ_PARTITIONED_METADATA_JSON,
1534 &[
1535 partitioned_add_json(
1536 "timestamp-ntz-pre-epoch.parquet",
1537 r#"{"event_ts_ntz":"1969-12-31 23:59:59.999999"}"#,
1538 ),
1539 partitioned_add_json(
1540 "timestamp-ntz-low-space.parquet",
1541 r#"{"event_ts_ntz":"2025-12-31 23:59:59.999999"}"#,
1542 ),
1543 partitioned_add_json(
1544 "timestamp-ntz-target-space.parquet",
1545 r#"{"event_ts_ntz":"2026-01-01 00:00:00.123456"}"#,
1546 ),
1547 partitioned_add_json(
1548 "timestamp-ntz-high.parquet",
1549 r#"{"event_ts_ntz":"2026-01-01 00:00:00.123457"}"#,
1550 ),
1551 partitioned_add_json("timestamp-ntz-null.parquet", r#"{"event_ts_ntz":null}"#),
1552 partitioned_add_json("timestamp-ntz-empty.parquet", r#"{"event_ts_ntz":""}"#),
1553 partitioned_add_json("timestamp-ntz-missing.parquet", r#"{}"#),
1554 ],
1555 )?;
1556 let source = load_delta_source(DeltaSourceConfig {
1557 name: "orders".to_owned(),
1558 table_uri: table.path.to_string_lossy().to_string(),
1559 version: None,
1560 storage_options: Default::default(),
1561 })?;
1562
1563 Ok((table, source))
1564 }
1565
1566 fn kernel_data_stats_characterization_source()
1567 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1568 let table = DeltaLogTable::new_with_metadata_and_adds(
1569 "kernel-data-stats-characterization",
1570 METADATA_JSON,
1571 &[
1572 add_json_with_id_stats("id-impossible.parquet", 10, 1, 50, 0),
1573 add_json_with_id_stats("id-possible.parquet", 10, 101, 150, 0),
1574 add_json("id-missing-stats.parquet"),
1575 ],
1576 )?;
1577 let source = load_delta_source(DeltaSourceConfig {
1578 name: "orders".to_owned(),
1579 table_uri: table.path.to_string_lossy().to_string(),
1580 version: None,
1581 storage_options: Default::default(),
1582 })?;
1583
1584 Ok((table, source))
1585 }
1586
1587 fn kernel_boolean_data_stats_characterization_source()
1588 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1589 let table = DeltaLogTable::new_with_metadata_and_adds(
1590 "kernel-boolean-data-stats-characterization",
1591 BOOLEAN_DATA_METADATA_JSON,
1592 &[
1593 add_json_with_boolean_stats("boolean-false-only.parquet", 10, false, false, 0),
1594 add_json_with_boolean_stats("boolean-true-only.parquet", 10, true, true, 0),
1595 add_json_with_boolean_stats("boolean-mixed.parquet", 10, false, true, 0),
1596 add_json_with_boolean_stats("boolean-false-with-null.parquet", 10, false, false, 2),
1597 add_json_with_boolean_stats("boolean-true-with-null.parquet", 10, true, true, 2),
1598 add_json_with_partial_boolean_stats(
1599 "boolean-all-null.parquet",
1600 10,
1601 None,
1602 None,
1603 Some(10),
1604 ),
1605 add_json("boolean-missing-stats.parquet"),
1606 ],
1607 )?;
1608 let source = load_delta_source(DeltaSourceConfig {
1609 name: "orders".to_owned(),
1610 table_uri: table.path.to_string_lossy().to_string(),
1611 version: None,
1612 storage_options: Default::default(),
1613 })?;
1614
1615 Ok((table, source))
1616 }
1617
1618 fn kernel_partial_boolean_data_stats_characterization_source()
1619 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1620 let table = DeltaLogTable::new_with_metadata_and_adds(
1621 "kernel-partial-boolean-data-stats-characterization",
1622 BOOLEAN_DATA_METADATA_JSON,
1623 &[
1624 add_json_with_partial_boolean_stats(
1625 "boolean-min-only-false.parquet",
1626 10,
1627 Some(false),
1628 None,
1629 Some(0),
1630 ),
1631 add_json_with_partial_boolean_stats(
1632 "boolean-max-only-true.parquet",
1633 10,
1634 None,
1635 Some(true),
1636 Some(0),
1637 ),
1638 add_json_with_partial_boolean_stats(
1639 "boolean-counts-only.parquet",
1640 10,
1641 None,
1642 None,
1643 Some(0),
1644 ),
1645 add_json_with_partial_boolean_stats(
1646 "boolean-missing-null-count.parquet",
1647 10,
1648 Some(false),
1649 Some(true),
1650 None,
1651 ),
1652 add_json("boolean-missing-stats.parquet"),
1653 ],
1654 )?;
1655 let source = load_delta_source(DeltaSourceConfig {
1656 name: "orders".to_owned(),
1657 table_uri: table.path.to_string_lossy().to_string(),
1658 version: None,
1659 storage_options: Default::default(),
1660 })?;
1661
1662 Ok((table, source))
1663 }
1664
1665 fn assert_boolean_stats_min_max_error(
1666 result: Result<Vec<String>, Box<dyn std::error::Error>>,
1667 ) -> Result<(), Box<dyn std::error::Error>> {
1668 let error = match result {
1669 Ok(paths) => {
1670 return Err(format!(
1671 "boolean min/max stats predicate should fail kernel scan: {paths:?}"
1672 )
1673 .into());
1674 }
1675 Err(error) => error,
1676 };
1677 let message = error.to_string();
1678 let debug_message = format!("{error:?}");
1679 assert!(
1680 message.contains("minValues") || debug_message.contains("minValues"),
1681 "{message}\n{debug_message}"
1682 );
1683
1684 Ok(())
1685 }
1686
1687 fn assert_binary_stats_min_max_error(
1688 result: Result<Vec<String>, Box<dyn std::error::Error>>,
1689 ) -> Result<(), Box<dyn std::error::Error>> {
1690 let error = match result {
1691 Ok(paths) => {
1692 return Err(format!(
1693 "binary min/max stats predicate should fail kernel scan: {paths:?}"
1694 )
1695 .into());
1696 }
1697 Err(error) => error,
1698 };
1699 let message = error.to_string();
1700 let debug_message = format!("{error:?}");
1701 assert!(
1702 message.contains("minValues")
1703 || message.contains("maxValues")
1704 || debug_message.contains("minValues")
1705 || debug_message.contains("maxValues"),
1706 "{message}\n{debug_message}"
1707 );
1708
1709 Ok(())
1710 }
1711
1712 fn assert_unsupported_literal_error(
1713 result: Result<Vec<String>, Box<dyn std::error::Error>>,
1714 ) -> Result<(), Box<dyn std::error::Error>> {
1715 let error = match result {
1716 Ok(paths) => {
1717 return Err(format!(
1718 "unsupported literal predicate should fail before kernel scan: {paths:?}"
1719 )
1720 .into());
1721 }
1722 Err(error) => error,
1723 };
1724 let message = error.to_string();
1725 let debug_message = format!("{error:?}");
1726 assert!(
1727 message.contains("unsupported DataFusion literal")
1728 || debug_message.contains("UnsupportedLiteral"),
1729 "{message}\n{debug_message}"
1730 );
1731
1732 Ok(())
1733 }
1734
1735 fn kernel_date_data_stats_characterization_source()
1736 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1737 let table = DeltaLogTable::new_with_metadata_and_adds(
1738 "kernel-date-data-stats-characterization",
1739 DATE_DATA_METADATA_JSON,
1740 &[
1741 add_json_with_date_stats(
1742 "date-pre-epoch-only.parquet",
1743 10,
1744 "1969-12-31",
1745 "1969-12-31",
1746 0,
1747 ),
1748 add_json_with_date_stats(
1749 "date-leap-only.parquet",
1750 10,
1751 "2024-02-29",
1752 "2024-02-29",
1753 0,
1754 ),
1755 add_json_with_date_stats(
1756 "date-new-year-only.parquet",
1757 10,
1758 "2026-01-01",
1759 "2026-01-01",
1760 0,
1761 ),
1762 add_json_with_date_stats("date-range.parquet", 10, "2024-02-29", "2026-01-01", 0),
1763 add_json_with_date_stats(
1764 "date-new-year-with-null.parquet",
1765 10,
1766 "2026-01-01",
1767 "2026-01-01",
1768 2,
1769 ),
1770 add_json_with_partial_date_stats("date-all-null.parquet", 10, None, None, Some(10)),
1771 add_json("date-missing-stats.parquet"),
1772 ],
1773 )?;
1774 let source = load_delta_source(DeltaSourceConfig {
1775 name: "orders".to_owned(),
1776 table_uri: table.path.to_string_lossy().to_string(),
1777 version: None,
1778 storage_options: Default::default(),
1779 })?;
1780
1781 Ok((table, source))
1782 }
1783
1784 fn kernel_partial_date_data_stats_characterization_source()
1785 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1786 let table = DeltaLogTable::new_with_metadata_and_adds(
1787 "kernel-partial-date-data-stats-characterization",
1788 DATE_DATA_METADATA_JSON,
1789 &[
1790 add_json_with_partial_date_stats(
1791 "date-min-only-high.parquet",
1792 10,
1793 Some("2026-01-01"),
1794 None,
1795 Some(0),
1796 ),
1797 add_json_with_partial_date_stats(
1798 "date-max-only-low.parquet",
1799 10,
1800 None,
1801 Some("2024-02-29"),
1802 Some(0),
1803 ),
1804 add_json_with_partial_date_stats(
1805 "date-counts-only.parquet",
1806 10,
1807 None,
1808 None,
1809 Some(0),
1810 ),
1811 add_json_with_partial_date_stats(
1812 "date-missing-null-count.parquet",
1813 10,
1814 Some("2024-02-29"),
1815 Some("2026-01-01"),
1816 None,
1817 ),
1818 add_json("date-missing-stats.parquet"),
1819 ],
1820 )?;
1821 let source = load_delta_source(DeltaSourceConfig {
1822 name: "orders".to_owned(),
1823 table_uri: table.path.to_string_lossy().to_string(),
1824 version: None,
1825 storage_options: Default::default(),
1826 })?;
1827
1828 Ok((table, source))
1829 }
1830
1831 fn kernel_decimal_data_stats_characterization_source()
1832 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1833 let table = DeltaLogTable::new_with_metadata_and_adds(
1834 "kernel-decimal-data-stats-characterization",
1835 DECIMAL_DATA_METADATA_JSON,
1836 &[
1837 add_json_with_decimal_stats(
1838 "decimal-negative-only.parquet",
1839 10,
1840 "-1.23",
1841 "-1.23",
1842 0,
1843 ),
1844 add_json_with_decimal_stats("decimal-zero-only.parquet", 10, "0.00", "0.00", 0),
1845 add_json_with_decimal_stats("decimal-two-only.parquet", 10, "2.00", "2.00", 0),
1846 add_json_with_decimal_stats("decimal-ten-only.parquet", 10, "10.00", "10.00", 0),
1847 add_json_with_decimal_stats(
1848 "decimal-large-only.parquet",
1849 10,
1850 "123.45",
1851 "123.45",
1852 0,
1853 ),
1854 add_json_with_decimal_stats("decimal-range.parquet", 10, "0.00", "10.00", 0),
1855 add_json_with_decimal_stats("decimal-two-with-null.parquet", 10, "2.00", "2.00", 2),
1856 add_json_with_partial_decimal_stats(
1857 "decimal-all-null.parquet",
1858 10,
1859 None,
1860 None,
1861 Some(10),
1862 ),
1863 add_json("decimal-missing-stats.parquet"),
1864 ],
1865 )?;
1866 let source = load_delta_source(DeltaSourceConfig {
1867 name: "orders".to_owned(),
1868 table_uri: table.path.to_string_lossy().to_string(),
1869 version: None,
1870 storage_options: Default::default(),
1871 })?;
1872
1873 Ok((table, source))
1874 }
1875
1876 fn kernel_partial_decimal_data_stats_characterization_source()
1877 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1878 let table = DeltaLogTable::new_with_metadata_and_adds(
1879 "kernel-partial-decimal-data-stats-characterization",
1880 DECIMAL_DATA_METADATA_JSON,
1881 &[
1882 add_json_with_partial_decimal_stats(
1883 "decimal-min-only-high.parquet",
1884 10,
1885 Some("10.00"),
1886 None,
1887 Some(0),
1888 ),
1889 add_json_with_partial_decimal_stats(
1890 "decimal-max-only-low.parquet",
1891 10,
1892 None,
1893 Some("0.00"),
1894 Some(0),
1895 ),
1896 add_json_with_partial_decimal_stats(
1897 "decimal-counts-only.parquet",
1898 10,
1899 None,
1900 None,
1901 Some(0),
1902 ),
1903 add_json_with_partial_decimal_stats(
1904 "decimal-missing-null-count.parquet",
1905 10,
1906 Some("0.00"),
1907 Some("10.00"),
1908 None,
1909 ),
1910 add_json("decimal-missing-stats.parquet"),
1911 ],
1912 )?;
1913 let source = load_delta_source(DeltaSourceConfig {
1914 name: "orders".to_owned(),
1915 table_uri: table.path.to_string_lossy().to_string(),
1916 version: None,
1917 storage_options: Default::default(),
1918 })?;
1919
1920 Ok((table, source))
1921 }
1922
1923 fn kernel_binary_data_stats_characterization_source()
1924 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1925 let table = DeltaLogTable::new_with_metadata_and_adds(
1926 "kernel-binary-data-stats-characterization",
1927 BINARY_DATA_METADATA_JSON,
1928 &[
1929 add_json_with_binary_stats("binary-HELLO.parquet", 10, "HELLO", "HELLO", 0),
1930 add_json_with_binary_stats("binary-empty.parquet", 10, "", "", 0),
1931 add_json_with_binary_stats("binary-hello.parquet", 10, "hello", "hello", 0),
1932 add_json_with_binary_stats("binary-range.parquet", 10, "a", "z", 0),
1933 add_json_with_binary_stats("binary-special.parquet", 10, "/=%", "/=%", 0),
1934 add_json_with_binary_stats("binary-with-null.parquet", 10, "hello", "hello", 2),
1935 add_json_with_partial_binary_stats(
1936 "binary-all-null.parquet",
1937 10,
1938 None,
1939 None,
1940 Some(10),
1941 ),
1942 add_json("binary-missing-stats.parquet"),
1943 ],
1944 )?;
1945 let source = load_delta_source(DeltaSourceConfig {
1946 name: "orders".to_owned(),
1947 table_uri: table.path.to_string_lossy().to_string(),
1948 version: None,
1949 storage_options: Default::default(),
1950 })?;
1951
1952 Ok((table, source))
1953 }
1954
1955 fn kernel_partial_binary_data_stats_characterization_source()
1956 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
1957 let table = DeltaLogTable::new_with_metadata_and_adds(
1958 "kernel-partial-binary-data-stats-characterization",
1959 BINARY_DATA_METADATA_JSON,
1960 &[
1961 add_json_with_partial_binary_stats(
1962 "binary-min-only-high.parquet",
1963 10,
1964 Some("m"),
1965 None,
1966 Some(0),
1967 ),
1968 add_json_with_partial_binary_stats(
1969 "binary-max-only-low.parquet",
1970 10,
1971 None,
1972 Some("a"),
1973 Some(0),
1974 ),
1975 add_json_with_partial_binary_stats(
1976 "binary-counts-only.parquet",
1977 10,
1978 None,
1979 None,
1980 Some(0),
1981 ),
1982 add_json_with_partial_binary_stats(
1983 "binary-missing-null-count.parquet",
1984 10,
1985 Some("a"),
1986 Some("z"),
1987 None,
1988 ),
1989 add_json("binary-missing-stats.parquet"),
1990 ],
1991 )?;
1992 let source = load_delta_source(DeltaSourceConfig {
1993 name: "orders".to_owned(),
1994 table_uri: table.path.to_string_lossy().to_string(),
1995 version: None,
1996 storage_options: Default::default(),
1997 })?;
1998
1999 Ok((table, source))
2000 }
2001
2002 fn kernel_floating_data_stats_characterization_source()
2003 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2004 let table = DeltaLogTable::new_with_metadata_and_adds(
2005 "kernel-floating-data-stats-characterization",
2006 FLOATING_DATA_METADATA_JSON,
2007 &[
2008 add_json_with_floating_stats(
2009 "floating-neg.parquet",
2010 10,
2011 "-1.5",
2012 "-1.5",
2013 "-2.25",
2014 "-2.25",
2015 0,
2016 ),
2017 add_json_with_floating_stats(
2018 "floating-neg-zero.parquet",
2019 10,
2020 "-0.0",
2021 "-0.0",
2022 "-0.0",
2023 "-0.0",
2024 0,
2025 ),
2026 add_json_with_floating_stats(
2027 "floating-pos-zero.parquet",
2028 10,
2029 "0.0",
2030 "0.0",
2031 "0.0",
2032 "0.0",
2033 0,
2034 ),
2035 add_json_with_floating_stats(
2036 "floating-one.parquet",
2037 10,
2038 "1.5",
2039 "1.5",
2040 "2.25",
2041 "2.25",
2042 0,
2043 ),
2044 add_json_with_floating_stats(
2045 "floating-range.parquet",
2046 10,
2047 "-1.0",
2048 "2.0",
2049 "-2.0",
2050 "3.0",
2051 0,
2052 ),
2053 add_json_with_floating_stats(
2054 "floating-ten.parquet",
2055 10,
2056 "10.0",
2057 "10.0",
2058 "10.0",
2059 "10.0",
2060 0,
2061 ),
2062 add_json_with_floating_stats(
2063 "floating-one-with-null.parquet",
2064 10,
2065 "1.5",
2066 "1.5",
2067 "2.25",
2068 "2.25",
2069 2,
2070 ),
2071 add_json_with_partial_floating_stats(
2072 "floating-all-null.parquet",
2073 10,
2074 None,
2075 None,
2076 None,
2077 None,
2078 Some(10),
2079 ),
2080 add_json("floating-missing-stats.parquet"),
2081 ],
2082 )?;
2083 let source = load_delta_source(DeltaSourceConfig {
2084 name: "orders".to_owned(),
2085 table_uri: table.path.to_string_lossy().to_string(),
2086 version: None,
2087 storage_options: Default::default(),
2088 })?;
2089
2090 Ok((table, source))
2091 }
2092
2093 fn kernel_partial_floating_data_stats_characterization_source()
2094 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2095 let table = DeltaLogTable::new_with_metadata_and_adds(
2096 "kernel-partial-floating-data-stats-characterization",
2097 FLOATING_DATA_METADATA_JSON,
2098 &[
2099 add_json_with_partial_floating_stats(
2100 "floating-min-only-high.parquet",
2101 10,
2102 Some("2.0"),
2103 None,
2104 Some("2.0"),
2105 None,
2106 Some(0),
2107 ),
2108 add_json_with_partial_floating_stats(
2109 "floating-max-only-low.parquet",
2110 10,
2111 None,
2112 Some("0.0"),
2113 None,
2114 Some("0.0"),
2115 Some(0),
2116 ),
2117 add_json_with_partial_floating_stats(
2118 "floating-counts-only.parquet",
2119 10,
2120 None,
2121 None,
2122 None,
2123 None,
2124 Some(0),
2125 ),
2126 add_json_with_partial_floating_stats(
2127 "floating-missing-null-count.parquet",
2128 10,
2129 Some("-1.0"),
2130 Some("2.0"),
2131 Some("-1.0"),
2132 Some("2.0"),
2133 None,
2134 ),
2135 add_json("floating-missing-stats.parquet"),
2136 ],
2137 )?;
2138 let source = load_delta_source(DeltaSourceConfig {
2139 name: "orders".to_owned(),
2140 table_uri: table.path.to_string_lossy().to_string(),
2141 version: None,
2142 storage_options: Default::default(),
2143 })?;
2144
2145 Ok((table, source))
2146 }
2147
2148 fn kernel_nonfinite_floating_data_stats_characterization_source()
2149 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2150 let table = DeltaLogTable::new_with_metadata_and_adds(
2151 "kernel-nonfinite-floating-data-stats-characterization",
2152 FLOATING_DATA_METADATA_JSON,
2153 &[
2154 add_json_with_floating_stats(
2155 "floating-valid.parquet",
2156 10,
2157 "1.5",
2158 "1.5",
2159 "2.25",
2160 "2.25",
2161 0,
2162 ),
2163 add_json_with_floating_stats(
2164 "floating-nan.parquet",
2165 10,
2166 "\\\"NaN\\\"",
2167 "\\\"NaN\\\"",
2168 "\\\"NaN\\\"",
2169 "\\\"NaN\\\"",
2170 0,
2171 ),
2172 add_json_with_floating_stats(
2173 "floating-inf.parquet",
2174 10,
2175 "\\\"Infinity\\\"",
2176 "\\\"Infinity\\\"",
2177 "\\\"Infinity\\\"",
2178 "\\\"Infinity\\\"",
2179 0,
2180 ),
2181 add_json_with_floating_stats(
2182 "floating-neg-inf.parquet",
2183 10,
2184 "\\\"-Infinity\\\"",
2185 "\\\"-Infinity\\\"",
2186 "\\\"-Infinity\\\"",
2187 "\\\"-Infinity\\\"",
2188 0,
2189 ),
2190 add_json("floating-missing-stats.parquet"),
2191 ],
2192 )?;
2193 let source = load_delta_source(DeltaSourceConfig {
2194 name: "orders".to_owned(),
2195 table_uri: table.path.to_string_lossy().to_string(),
2196 version: None,
2197 storage_options: Default::default(),
2198 })?;
2199
2200 Ok((table, source))
2201 }
2202
2203 fn kernel_string_data_stats_characterization_source()
2204 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2205 let table = DeltaLogTable::new_with_metadata_and_adds(
2206 "kernel-string-data-stats-characterization",
2207 STRING_DATA_METADATA_JSON,
2208 &[
2209 add_json_with_string_stats("string-empty-only.parquet", 10, "", "", 0),
2210 add_json_with_string_stats(
2211 "string-mixed-case-only.parquet",
2212 10,
2213 "Alice",
2214 "Alice",
2215 0,
2216 ),
2217 add_json_with_string_stats("string-alice-only.parquet", 10, "alice", "alice", 0),
2218 add_json_with_string_stats("string-bob-only.parquet", 10, "bob", "bob", 0),
2219 add_json_with_string_stats("string-range.parquet", 10, "alice", "morgan", 0),
2220 add_json_with_string_stats("string-zed-only.parquet", 10, "zed", "zed", 0),
2221 add_json_with_string_stats(
2222 "string-alice-with-null.parquet",
2223 10,
2224 "alice",
2225 "alice",
2226 2,
2227 ),
2228 add_json_with_partial_string_stats(
2229 "string-all-null.parquet",
2230 10,
2231 None,
2232 None,
2233 Some(10),
2234 ),
2235 add_json("string-missing-stats.parquet"),
2236 ],
2237 )?;
2238 let source = load_delta_source(DeltaSourceConfig {
2239 name: "orders".to_owned(),
2240 table_uri: table.path.to_string_lossy().to_string(),
2241 version: None,
2242 storage_options: Default::default(),
2243 })?;
2244
2245 Ok((table, source))
2246 }
2247
2248 fn kernel_partial_string_data_stats_characterization_source()
2249 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2250 let table = DeltaLogTable::new_with_metadata_and_adds(
2251 "kernel-partial-string-data-stats-characterization",
2252 STRING_DATA_METADATA_JSON,
2253 &[
2254 add_json_with_partial_string_stats(
2255 "string-min-only-morgan.parquet",
2256 10,
2257 Some("morgan"),
2258 None,
2259 Some(0),
2260 ),
2261 add_json_with_partial_string_stats(
2262 "string-max-only-alice.parquet",
2263 10,
2264 None,
2265 Some("alice"),
2266 Some(0),
2267 ),
2268 add_json_with_partial_string_stats(
2269 "string-counts-only.parquet",
2270 10,
2271 None,
2272 None,
2273 Some(0),
2274 ),
2275 add_json_with_partial_string_stats(
2276 "string-missing-null-count.parquet",
2277 10,
2278 Some("alice"),
2279 Some("morgan"),
2280 None,
2281 ),
2282 add_json("string-missing-stats.parquet"),
2283 ],
2284 )?;
2285 let source = load_delta_source(DeltaSourceConfig {
2286 name: "orders".to_owned(),
2287 table_uri: table.path.to_string_lossy().to_string(),
2288 version: None,
2289 storage_options: Default::default(),
2290 })?;
2291
2292 Ok((table, source))
2293 }
2294
2295 fn kernel_non_ascii_string_data_stats_characterization_source()
2296 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2297 let table = DeltaLogTable::new_with_metadata_and_adds(
2298 "kernel-non-ascii-string-data-stats-characterization",
2299 STRING_DATA_METADATA_JSON,
2300 &[
2301 add_json_with_string_stats("string-ascii-cafe.parquet", 10, "cafe", "cafe", 0),
2302 add_json_with_string_stats("string-ascii-zulu.parquet", 10, "zulu", "zulu", 0),
2303 add_json_with_string_stats(
2304 "string-eclair.parquet",
2305 10,
2306 "\\u00e9clair",
2307 "\\u00e9clair",
2308 0,
2309 ),
2310 add_json_with_string_stats(
2311 "string-emile.parquet",
2312 10,
2313 "\\u00e9mile",
2314 "\\u00e9mile",
2315 0,
2316 ),
2317 add_json("string-missing-stats.parquet"),
2318 ],
2319 )?;
2320 let source = load_delta_source(DeltaSourceConfig {
2321 name: "orders".to_owned(),
2322 table_uri: table.path.to_string_lossy().to_string(),
2323 version: None,
2324 storage_options: Default::default(),
2325 })?;
2326
2327 Ok((table, source))
2328 }
2329
2330 fn kernel_timestamp_data_stats_characterization_source()
2331 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2332 let table = DeltaLogTable::new_with_metadata_and_adds(
2333 "kernel-timestamp-data-stats-characterization",
2334 TIMESTAMP_DATA_METADATA_JSON,
2335 &[
2336 add_json_with_timestamp_stats(
2337 "timestamp-pre-epoch-only.parquet",
2338 "event_ts",
2339 10,
2340 "1969-12-31T23:59:59.999999Z",
2341 "1969-12-31T23:59:59.999999Z",
2342 0,
2343 ),
2344 add_json_with_timestamp_stats(
2345 "timestamp-low-only.parquet",
2346 "event_ts",
2347 10,
2348 "2025-12-31T23:59:59.999999Z",
2349 "2025-12-31T23:59:59.999999Z",
2350 0,
2351 ),
2352 add_json_with_timestamp_stats(
2353 "timestamp-target-only.parquet",
2354 "event_ts",
2355 10,
2356 "2026-01-01T00:00:00.123456Z",
2357 "2026-01-01T00:00:00.123456Z",
2358 0,
2359 ),
2360 add_json_with_timestamp_stats(
2361 "timestamp-high-only.parquet",
2362 "event_ts",
2363 10,
2364 "2026-01-01T00:00:00.123457Z",
2365 "2026-01-01T00:00:00.123457Z",
2366 0,
2367 ),
2368 add_json_with_timestamp_stats(
2369 "timestamp-range.parquet",
2370 "event_ts",
2371 10,
2372 "2025-12-31T23:59:59.999999Z",
2373 "2026-01-01T00:00:00.123456Z",
2374 0,
2375 ),
2376 add_json_with_timestamp_stats(
2377 "timestamp-target-with-null.parquet",
2378 "event_ts",
2379 10,
2380 "2026-01-01T00:00:00.123456Z",
2381 "2026-01-01T00:00:00.123456Z",
2382 2,
2383 ),
2384 add_json_with_partial_timestamp_stats(
2385 "timestamp-all-null.parquet",
2386 "event_ts",
2387 10,
2388 None,
2389 None,
2390 Some(10),
2391 ),
2392 add_json("timestamp-missing-stats.parquet"),
2393 ],
2394 )?;
2395 let source = load_delta_source(DeltaSourceConfig {
2396 name: "orders".to_owned(),
2397 table_uri: table.path.to_string_lossy().to_string(),
2398 version: None,
2399 storage_options: Default::default(),
2400 })?;
2401
2402 Ok((table, source))
2403 }
2404
2405 fn kernel_timestamp_ntz_data_stats_characterization_source()
2406 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2407 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
2408 "kernel-timestamp-ntz-data-stats-characterization",
2409 TIMESTAMP_NTZ_PROTOCOL_JSON,
2410 TIMESTAMP_NTZ_DATA_METADATA_JSON,
2411 &[
2412 add_json_with_timestamp_stats(
2413 "timestamp-ntz-pre-epoch-only.parquet",
2414 "event_ts_ntz",
2415 10,
2416 "1969-12-31 23:59:59.999999",
2417 "1969-12-31 23:59:59.999999",
2418 0,
2419 ),
2420 add_json_with_timestamp_stats(
2421 "timestamp-ntz-low-only.parquet",
2422 "event_ts_ntz",
2423 10,
2424 "2025-12-31 23:59:59.999999",
2425 "2025-12-31 23:59:59.999999",
2426 0,
2427 ),
2428 add_json_with_timestamp_stats(
2429 "timestamp-ntz-target-only.parquet",
2430 "event_ts_ntz",
2431 10,
2432 "2026-01-01 00:00:00.123456",
2433 "2026-01-01 00:00:00.123456",
2434 0,
2435 ),
2436 add_json_with_timestamp_stats(
2437 "timestamp-ntz-high-only.parquet",
2438 "event_ts_ntz",
2439 10,
2440 "2026-01-01 00:00:00.123457",
2441 "2026-01-01 00:00:00.123457",
2442 0,
2443 ),
2444 add_json_with_timestamp_stats(
2445 "timestamp-ntz-range.parquet",
2446 "event_ts_ntz",
2447 10,
2448 "2025-12-31 23:59:59.999999",
2449 "2026-01-01 00:00:00.123456",
2450 0,
2451 ),
2452 add_json_with_timestamp_stats(
2453 "timestamp-ntz-target-with-null.parquet",
2454 "event_ts_ntz",
2455 10,
2456 "2026-01-01 00:00:00.123456",
2457 "2026-01-01 00:00:00.123456",
2458 2,
2459 ),
2460 add_json_with_partial_timestamp_stats(
2461 "timestamp-ntz-all-null.parquet",
2462 "event_ts_ntz",
2463 10,
2464 None,
2465 None,
2466 Some(10),
2467 ),
2468 add_json("timestamp-ntz-missing-stats.parquet"),
2469 ],
2470 )?;
2471 let source = load_delta_source(DeltaSourceConfig {
2472 name: "orders".to_owned(),
2473 table_uri: table.path.to_string_lossy().to_string(),
2474 version: None,
2475 storage_options: Default::default(),
2476 })?;
2477
2478 Ok((table, source))
2479 }
2480
2481 fn kernel_partial_timestamp_data_stats_characterization_source()
2482 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2483 let table = DeltaLogTable::new_with_metadata_and_adds(
2484 "kernel-partial-timestamp-data-stats-characterization",
2485 TIMESTAMP_DATA_METADATA_JSON,
2486 &[
2487 add_json_with_partial_timestamp_stats(
2488 "timestamp-min-only-target.parquet",
2489 "event_ts",
2490 10,
2491 Some("2026-01-01T00:00:00.123456Z"),
2492 None,
2493 Some(0),
2494 ),
2495 add_json_with_partial_timestamp_stats(
2496 "timestamp-max-only-low.parquet",
2497 "event_ts",
2498 10,
2499 None,
2500 Some("2025-12-31T23:59:59.999999Z"),
2501 Some(0),
2502 ),
2503 add_json_with_partial_timestamp_stats(
2504 "timestamp-counts-only.parquet",
2505 "event_ts",
2506 10,
2507 None,
2508 None,
2509 Some(0),
2510 ),
2511 add_json_with_partial_timestamp_stats(
2512 "timestamp-missing-null-count.parquet",
2513 "event_ts",
2514 10,
2515 Some("2025-12-31T23:59:59.999999Z"),
2516 Some("2026-01-01T00:00:00.123456Z"),
2517 None,
2518 ),
2519 add_json("timestamp-missing-stats.parquet"),
2520 ],
2521 )?;
2522 let source = load_delta_source(DeltaSourceConfig {
2523 name: "orders".to_owned(),
2524 table_uri: table.path.to_string_lossy().to_string(),
2525 version: None,
2526 storage_options: Default::default(),
2527 })?;
2528
2529 Ok((table, source))
2530 }
2531
2532 fn kernel_partial_timestamp_ntz_data_stats_characterization_source()
2533 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2534 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
2535 "kernel-partial-timestamp-ntz-data-stats-characterization",
2536 TIMESTAMP_NTZ_PROTOCOL_JSON,
2537 TIMESTAMP_NTZ_DATA_METADATA_JSON,
2538 &[
2539 add_json_with_partial_timestamp_stats(
2540 "timestamp-ntz-min-only-target.parquet",
2541 "event_ts_ntz",
2542 10,
2543 Some("2026-01-01 00:00:00.123456"),
2544 None,
2545 Some(0),
2546 ),
2547 add_json_with_partial_timestamp_stats(
2548 "timestamp-ntz-max-only-low.parquet",
2549 "event_ts_ntz",
2550 10,
2551 None,
2552 Some("2025-12-31 23:59:59.999999"),
2553 Some(0),
2554 ),
2555 add_json_with_partial_timestamp_stats(
2556 "timestamp-ntz-counts-only.parquet",
2557 "event_ts_ntz",
2558 10,
2559 None,
2560 None,
2561 Some(0),
2562 ),
2563 add_json_with_partial_timestamp_stats(
2564 "timestamp-ntz-missing-null-count.parquet",
2565 "event_ts_ntz",
2566 10,
2567 Some("2025-12-31 23:59:59.999999"),
2568 Some("2026-01-01 00:00:00.123456"),
2569 None,
2570 ),
2571 add_json("timestamp-ntz-missing-stats.parquet"),
2572 ],
2573 )?;
2574 let source = load_delta_source(DeltaSourceConfig {
2575 name: "orders".to_owned(),
2576 table_uri: table.path.to_string_lossy().to_string(),
2577 version: None,
2578 storage_options: Default::default(),
2579 })?;
2580
2581 Ok((table, source))
2582 }
2583
2584 fn kernel_all_sufficient_data_stats_source()
2585 -> Result<(DeltaLogTable, super::PlannedDeltaSource), Box<dyn std::error::Error>> {
2586 let table = DeltaLogTable::new_with_metadata_and_adds(
2587 "kernel-all-sufficient-data-stats",
2588 METADATA_JSON,
2589 &[
2590 add_json_with_id_stats("id-low-a.parquet", 10, 1, 50, 0),
2591 add_json_with_id_stats("id-low-b.parquet", 10, 51, 100, 0),
2592 ],
2593 )?;
2594 let source = load_delta_source(DeltaSourceConfig {
2595 name: "orders".to_owned(),
2596 table_uri: table.path.to_string_lossy().to_string(),
2597 version: None,
2598 storage_options: Default::default(),
2599 })?;
2600
2601 Ok((table, source))
2602 }
2603
2604 fn kernel_predicated_file_paths(
2605 source: &super::PlannedDeltaSource,
2606 filter: &datafusion::logical_expr::Expr,
2607 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
2608 let predicate = datafusion_expr_to_kernel_predicate(filter)?;
2609 kernel_predicate_file_paths(source, predicate)
2610 }
2611
2612 fn kernel_predicate_file_paths(
2613 source: &super::PlannedDeltaSource,
2614 predicate: DeltaKernelPredicate,
2615 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
2616 let scan = build_projected_predicated_delta_scan(source, None, Some(predicate))?;
2617
2618 kernel_scan_file_paths(&scan, source.table_uri())
2619 }
2620
2621 fn build_projected_predicated_stats_delta_scan(
2622 source: &super::PlannedDeltaSource,
2623 predicate: Option<DeltaKernelPredicate>,
2624 ) -> Result<ProjectedDeltaScan, delta_kernel::Error> {
2625 let (scan, kernel_schema) = super::kernel::build_projected_predicated_stats_scan(
2626 source.loaded_snapshot().kernel_snapshot(),
2627 None,
2628 predicate,
2629 )?;
2630
2631 Ok(ProjectedDeltaScan {
2632 scan,
2633 kernel_schema,
2634 })
2635 }
2636
2637 fn kernel_predicated_stats_file_paths(
2638 source: &super::PlannedDeltaSource,
2639 filter: &datafusion::logical_expr::Expr,
2640 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
2641 let predicate = datafusion_expr_to_kernel_predicate(filter)?;
2642 let scan = build_projected_predicated_stats_delta_scan(source, Some(predicate))?;
2643
2644 kernel_scan_file_paths(&scan, source.table_uri())
2645 }
2646
2647 #[derive(Debug, Default, PartialEq, Eq)]
2648 struct KernelStatsMetadataBoundary {
2649 selected_rows: usize,
2650 selected_stats_rows: usize,
2651 selected_missing_stats_rows: usize,
2652 has_id_min_values: bool,
2653 has_id_max_values: bool,
2654 has_id_null_count: bool,
2655 }
2656
2657 fn kernel_stats_metadata_boundary(
2658 scan: &ProjectedDeltaScan,
2659 table_uri: &str,
2660 ) -> Result<KernelStatsMetadataBoundary, Box<dyn std::error::Error>> {
2661 let storage_options = DeltaStorageOptions::default();
2662 let table_url = super::kernel::try_parse_uri(table_uri)?;
2663 let store = super::kernel::store_from_url_opts(
2664 &table_url,
2665 storage_options
2666 .iter()
2667 .map(|(key, value)| (key.as_str(), value.as_str())),
2668 )?;
2669 let engine = super::kernel::DefaultEngineBuilder::new(store).build();
2670 let mut boundary = KernelStatsMetadataBoundary::default();
2671
2672 for scan_metadata in scan.kernel_scan().scan_metadata(&engine)? {
2673 let (underlying_data, selection_vector) = scan_metadata?.scan_files.into_parts();
2674 let batch: RecordBatch =
2675 super::kernel::ArrowEngineData::try_from_engine_data(underlying_data)?.into();
2676 let stats_parsed = stats_parsed_column(&batch)?;
2677 let min_values = stats_struct_child(stats_parsed, "minValues")?;
2678 let max_values = stats_struct_child(stats_parsed, "maxValues")?;
2679 let null_count = stats_struct_child(stats_parsed, "nullCount")?;
2680 let min_id = stats_struct_child_column(min_values, "id")?;
2681 let max_id = stats_struct_child_column(max_values, "id")?;
2682 let null_count_id = stats_struct_child_column(null_count, "id")?;
2683
2684 boundary.has_id_min_values |= min_values.column_by_name("id").is_some();
2685 boundary.has_id_max_values |= max_values.column_by_name("id").is_some();
2686 boundary.has_id_null_count |= null_count.column_by_name("id").is_some();
2687
2688 for (row_index, selected) in selection_vector.iter().copied().enumerate() {
2689 if selected {
2690 boundary.selected_rows += 1;
2691 if !min_id.is_null(row_index)
2692 && !max_id.is_null(row_index)
2693 && !null_count_id.is_null(row_index)
2694 {
2695 boundary.selected_stats_rows += 1;
2696 } else {
2697 boundary.selected_missing_stats_rows += 1;
2698 }
2699 }
2700 }
2701 }
2702
2703 Ok(boundary)
2704 }
2705
2706 fn stats_parsed_column(
2707 batch: &RecordBatch,
2708 ) -> Result<&StructArray, Box<dyn std::error::Error>> {
2709 let Some(column) = batch.column_by_name("stats_parsed") else {
2710 return Err("scan metadata did not expose stats_parsed".into());
2711 };
2712 let Some(stats_parsed) = column.as_any().downcast_ref::<StructArray>() else {
2713 return Err("scan metadata stats_parsed was not a struct array".into());
2714 };
2715
2716 Ok(stats_parsed)
2717 }
2718
2719 fn stats_struct_child<'a>(
2720 stats_parsed: &'a StructArray,
2721 name: &str,
2722 ) -> Result<&'a StructArray, Box<dyn std::error::Error>> {
2723 let Some(column) = stats_parsed.column_by_name(name) else {
2724 return Err(format!("scan metadata stats_parsed did not expose {name}").into());
2725 };
2726 let Some(stats_child) = column.as_any().downcast_ref::<StructArray>() else {
2727 return Err(format!("scan metadata stats_parsed.{name} was not a struct array").into());
2728 };
2729
2730 Ok(stats_child)
2731 }
2732
2733 fn stats_struct_child_column<'a>(
2734 stats_child: &'a StructArray,
2735 name: &str,
2736 ) -> Result<&'a dyn Array, Box<dyn std::error::Error>> {
2737 let Some(column) = stats_child.column_by_name(name) else {
2738 return Err(format!("scan metadata stats_parsed child did not expose {name}").into());
2739 };
2740
2741 Ok(column.as_ref())
2742 }
2743
2744 fn int8_lit(value: i8) -> Expr {
2745 Expr::Literal(ScalarValue::Int8(Some(value)), None)
2746 }
2747
2748 fn int16_lit(value: i16) -> Expr {
2749 Expr::Literal(ScalarValue::Int16(Some(value)), None)
2750 }
2751
2752 fn int32_lit(value: i32) -> Expr {
2753 Expr::Literal(ScalarValue::Int32(Some(value)), None)
2754 }
2755
2756 fn int64_lit(value: i64) -> Expr {
2757 Expr::Literal(ScalarValue::Int64(Some(value)), None)
2758 }
2759
2760 fn bool_lit(value: bool) -> Expr {
2761 Expr::Literal(ScalarValue::Boolean(Some(value)), None)
2762 }
2763
2764 fn date_lit(value: i32) -> Expr {
2765 Expr::Literal(ScalarValue::Date32(Some(value)), None)
2766 }
2767
2768 fn timestamp_lit(value: i64, timezone: &str) -> Expr {
2769 Expr::Literal(
2770 ScalarValue::TimestampMicrosecond(Some(value), Some(timezone.into())),
2771 None,
2772 )
2773 }
2774
2775 fn timestamp_ntz_lit(value: i64) -> Expr {
2776 Expr::Literal(ScalarValue::TimestampMicrosecond(Some(value), None), None)
2777 }
2778
2779 fn decimal_lit(value: i128) -> Expr {
2780 Expr::Literal(ScalarValue::Decimal128(Some(value), 10, 2), None)
2781 }
2782
2783 fn decimal_lit_with_type(value: i128, precision: u8, scale: i8) -> Expr {
2784 Expr::Literal(ScalarValue::Decimal128(Some(value), precision, scale), None)
2785 }
2786
2787 fn string_lit(value: &str) -> Expr {
2788 Expr::Literal(ScalarValue::Utf8(Some(value.to_owned())), None)
2789 }
2790
2791 fn binary_lit(value: &[u8]) -> Expr {
2792 Expr::Literal(ScalarValue::Binary(Some(value.to_vec())), None)
2793 }
2794
2795 fn large_string_lit(value: &str) -> Expr {
2796 Expr::Literal(ScalarValue::LargeUtf8(Some(value.to_owned())), None)
2797 }
2798
2799 fn float32_lit(value: f32) -> Expr {
2800 Expr::Literal(ScalarValue::Float32(Some(value)), None)
2801 }
2802
2803 fn float64_lit(value: f64) -> Expr {
2804 Expr::Literal(ScalarValue::Float64(Some(value)), None)
2805 }
2806
2807 fn binary_partition_column() -> Expression {
2808 Expression::Column(ColumnName::new(["payload"]))
2809 }
2810
2811 fn binary_partition_value(value: &[u8]) -> Expression {
2812 Expression::Literal(Scalar::Binary(value.to_vec()))
2813 }
2814
2815 fn binary_partition_eq(value: &[u8]) -> DeltaKernelPredicate {
2816 DeltaKernelPredicate::new(Predicate::eq(
2817 binary_partition_column(),
2818 binary_partition_value(value),
2819 ))
2820 }
2821
2822 fn binary_partition_ne(value: &[u8]) -> DeltaKernelPredicate {
2823 DeltaKernelPredicate::new(Predicate::ne(
2824 binary_partition_column(),
2825 binary_partition_value(value),
2826 ))
2827 }
2828
2829 fn timestamp_partition_column() -> Expression {
2830 Expression::Column(ColumnName::new(["event_ts"]))
2831 }
2832
2833 fn timestamp_partition_value(value: i64) -> Expression {
2834 Expression::Literal(Scalar::Timestamp(value))
2835 }
2836
2837 fn timestamp_partition_eq(value: i64) -> DeltaKernelPredicate {
2838 DeltaKernelPredicate::new(Predicate::eq(
2839 timestamp_partition_column(),
2840 timestamp_partition_value(value),
2841 ))
2842 }
2843
2844 fn timestamp_partition_ne(value: i64) -> DeltaKernelPredicate {
2845 DeltaKernelPredicate::new(Predicate::ne(
2846 timestamp_partition_column(),
2847 timestamp_partition_value(value),
2848 ))
2849 }
2850
2851 fn timestamp_ntz_partition_column() -> Expression {
2852 Expression::Column(ColumnName::new(["event_ts_ntz"]))
2853 }
2854
2855 fn timestamp_ntz_partition_value(value: i64) -> Expression {
2856 Expression::Literal(Scalar::TimestampNtz(value))
2857 }
2858
2859 fn timestamp_ntz_partition_eq(value: i64) -> DeltaKernelPredicate {
2860 DeltaKernelPredicate::new(Predicate::eq(
2861 timestamp_ntz_partition_column(),
2862 timestamp_ntz_partition_value(value),
2863 ))
2864 }
2865
2866 fn timestamp_ntz_partition_ne(value: i64) -> DeltaKernelPredicate {
2867 DeltaKernelPredicate::new(Predicate::ne(
2868 timestamp_ntz_partition_column(),
2869 timestamp_ntz_partition_value(value),
2870 ))
2871 }
2872
2873 fn assert_invalid_integer_partition_error(
2874 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2875 ) -> Result<(), Box<dyn std::error::Error>> {
2876 let error = match result {
2877 Ok(paths) => {
2878 return Err(
2879 format!("invalid integer metadata should fail kernel scan: {paths:?}").into(),
2880 );
2881 }
2882 Err(error) => error,
2883 };
2884 let message = error.to_string();
2885 let debug_message = format!("{error:?}");
2886 assert!(message.contains("not-an-integer"));
2887 assert!(debug_message.contains("Primitive(Long)"));
2888
2889 Ok(())
2890 }
2891
2892 fn assert_invalid_boolean_partition_error(
2893 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2894 ) -> Result<(), Box<dyn std::error::Error>> {
2895 let error = match result {
2896 Ok(paths) => {
2897 return Err(
2898 format!("invalid boolean metadata should fail kernel scan: {paths:?}").into(),
2899 );
2900 }
2901 Err(error) => error,
2902 };
2903 let message = error.to_string();
2904 let debug_message = format!("{error:?}");
2905 assert!(message.contains("not-a-boolean"));
2906 assert!(debug_message.contains("Primitive(Boolean)"));
2907
2908 Ok(())
2909 }
2910
2911 fn assert_invalid_date_partition_error(
2912 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2913 ) -> Result<(), Box<dyn std::error::Error>> {
2914 let error = match result {
2915 Ok(paths) => {
2916 return Err(
2917 format!("invalid date metadata should fail kernel scan: {paths:?}").into(),
2918 );
2919 }
2920 Err(error) => error,
2921 };
2922 let message = error.to_string();
2923 let debug_message = format!("{error:?}");
2924 assert!(message.contains("not-a-date"));
2925 assert!(debug_message.contains("Primitive(Date)"));
2926
2927 Ok(())
2928 }
2929
2930 fn assert_invalid_decimal_partition_error(
2931 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2932 ) -> Result<(), Box<dyn std::error::Error>> {
2933 let error = match result {
2934 Ok(paths) => {
2935 return Err(
2936 format!("invalid decimal metadata should fail kernel scan: {paths:?}").into(),
2937 );
2938 }
2939 Err(error) => error,
2940 };
2941 let message = error.to_string();
2942 let debug_message = format!("{error:?}");
2943 assert!(
2944 message.contains("not-a-decimal")
2945 || message.contains("123.450")
2946 || message.contains("Could not parse int")
2947 || debug_message.contains("ParseIntError")
2948 || debug_message.contains("InvalidDecimal"),
2949 "{message}\n{debug_message}"
2950 );
2951 assert!(
2952 debug_message.contains("Decimal") || debug_message.contains("ParseIntError"),
2953 "{debug_message}"
2954 );
2955
2956 Ok(())
2957 }
2958
2959 fn assert_invalid_floating_partition_error(
2960 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2961 ) -> Result<(), Box<dyn std::error::Error>> {
2962 let error = match result {
2963 Ok(paths) => {
2964 return Err(format!(
2965 "invalid floating metadata should fail kernel scan: {paths:?}"
2966 )
2967 .into());
2968 }
2969 Err(error) => error,
2970 };
2971 let message = error.to_string();
2972 let debug_message = format!("{error:?}");
2973 assert!(
2974 message.contains("not-a-float")
2975 || message.contains("not-a-double")
2976 || debug_message.contains("InvalidFloat")
2977 || debug_message.contains("ParseFloatError"),
2978 "{message}\n{debug_message}"
2979 );
2980
2981 Ok(())
2982 }
2983
2984 fn assert_invalid_timestamp_partition_error(
2985 result: Result<Vec<String>, Box<dyn std::error::Error>>,
2986 ) -> Result<(), Box<dyn std::error::Error>> {
2987 let error = match result {
2988 Ok(paths) => {
2989 return Err(format!(
2990 "invalid timestamp metadata should fail kernel scan: {paths:?}"
2991 )
2992 .into());
2993 }
2994 Err(error) => error,
2995 };
2996 let message = error.to_string();
2997 let debug_message = format!("{error:?}");
2998 assert!(
2999 message.contains("not-a-timestamp")
3000 || debug_message.contains("not-a-timestamp")
3001 || debug_message.contains("Timestamp"),
3002 "{message}\n{debug_message}"
3003 );
3004
3005 Ok(())
3006 }
3007
3008 fn assert_invalid_timestamp_ntz_partition_error(
3009 result: Result<Vec<String>, Box<dyn std::error::Error>>,
3010 ) -> Result<(), Box<dyn std::error::Error>> {
3011 let error = match result {
3012 Ok(paths) => {
3013 return Err(format!(
3014 "invalid timestamp_ntz metadata should fail kernel scan: {paths:?}"
3015 )
3016 .into());
3017 }
3018 Err(error) => error,
3019 };
3020 let message = error.to_string();
3021 let debug_message = format!("{error:?}");
3022 assert!(
3023 message.contains("not-a-timestamp")
3024 || debug_message.contains("not-a-timestamp")
3025 || debug_message.contains("TimestampNtz"),
3026 "{message}\n{debug_message}"
3027 );
3028
3029 Ok(())
3030 }
3031
3032 #[test]
3033 fn kernel_partition_characterization_uses_official_delta_kernel_0_23_0() {
3034 let manifest = include_str!("../../Cargo.toml");
3035 let lockfile = include_str!("../../../../Cargo.lock");
3036
3037 assert!(manifest.contains(r#"delta_kernel = { version = "0.23.0""#));
3038 assert!(lockfile.contains(
3039 "name = \"delta_kernel\"\nversion = \"0.23.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\""
3040 ));
3041 assert!(!manifest.contains("deltalake"));
3042 assert!(!manifest.contains("buoyant_kernel"));
3043 }
3044
3045 #[test]
3046 fn kernel_predicated_scan_prunes_files() -> Result<(), Box<dyn std::error::Error>> {
3047 let table = DeltaLogTable::new_with_metadata_and_adds(
3048 "kernel-predicated-scan-files",
3049 PARTITIONED_METADATA_JSON,
3050 &[
3051 partitioned_add_json("region-us-west.parquet", r#"{"region":"us-west"}"#),
3052 partitioned_add_json("region-us-east.parquet", r#"{"region":"us-east"}"#),
3053 ],
3054 )?;
3055 let source = load_delta_source(DeltaSourceConfig {
3056 name: "orders".to_owned(),
3057 table_uri: table.path.to_string_lossy().to_string(),
3058 version: None,
3059 storage_options: Default::default(),
3060 })?;
3061 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
3062 let predicate = datafusion_expr_to_kernel_predicate(&col("region").eq(lit("us-west")))?;
3063 let predicated_scan =
3064 build_projected_predicated_delta_scan(&source, None, Some(predicate))?;
3065
3066 assert_eq!(
3067 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
3068 vec!["region-us-east.parquet", "region-us-west.parquet"]
3069 );
3070 assert_eq!(
3071 kernel_scan_file_paths(&predicated_scan, source.table_uri())?,
3072 vec!["region-us-west.parquet"]
3073 );
3074
3075 Ok(())
3076 }
3077
3078 #[test]
3079 fn kernel_stats_scan_metadata_exposes_parsed_file_stats_without_data_file_reads()
3080 -> Result<(), Box<dyn std::error::Error>> {
3081 let (table, source) = kernel_data_stats_characterization_source()?;
3082 let stats_scan = build_projected_predicated_stats_delta_scan(&source, None)?;
3083
3084 assert!(!table.path.join("id-impossible.parquet").exists());
3085 assert!(!table.path.join("id-possible.parquet").exists());
3086 assert!(!table.path.join("id-missing-stats.parquet").exists());
3087 assert_eq!(
3088 kernel_scan_file_paths(&stats_scan, source.table_uri())?,
3089 vec![
3090 "id-impossible.parquet",
3091 "id-missing-stats.parquet",
3092 "id-possible.parquet",
3093 ]
3094 );
3095 assert_eq!(
3096 kernel_stats_metadata_boundary(&stats_scan, source.table_uri())?,
3097 KernelStatsMetadataBoundary {
3098 selected_rows: 3,
3099 selected_stats_rows: 2,
3100 selected_missing_stats_rows: 1,
3101 has_id_min_values: true,
3102 has_id_max_values: true,
3103 has_id_null_count: true,
3104 }
3105 );
3106
3107 Ok(())
3108 }
3109
3110 #[test]
3111 fn kernel_data_column_stats_pruning_keeps_possible_and_missing_stats_files()
3112 -> Result<(), Box<dyn std::error::Error>> {
3113 let (_table, source) = kernel_data_stats_characterization_source()?;
3114
3115 assert_eq!(
3116 kernel_predicated_stats_file_paths(&source, &col("id").gt(int32_lit(100)))?,
3117 vec!["id-missing-stats.parquet", "id-possible.parquet"]
3118 );
3119
3120 Ok(())
3121 }
3122
3123 #[test]
3124 fn kernel_data_column_stats_pruning_can_return_empty_when_all_stats_are_impossible()
3125 -> Result<(), Box<dyn std::error::Error>> {
3126 let (_table, source) = kernel_all_sufficient_data_stats_source()?;
3127
3128 assert_eq!(
3129 kernel_predicated_stats_file_paths(&source, &col("id").gt(int32_lit(100)))?,
3130 Vec::<String>::new()
3131 );
3132
3133 Ok(())
3134 }
3135
3136 #[test]
3137 fn kernel_boolean_data_column_stats_pruning_documents_min_max_boundary()
3138 -> Result<(), Box<dyn std::error::Error>> {
3139 let (_table, source) = kernel_boolean_data_stats_characterization_source()?;
3140
3141 assert_boolean_stats_min_max_error(kernel_predicated_stats_file_paths(
3142 &source,
3143 &col("is_current").eq(bool_lit(true)),
3144 ))?;
3145 assert_boolean_stats_min_max_error(kernel_predicated_stats_file_paths(
3146 &source,
3147 &col("is_current").eq(bool_lit(false)),
3148 ))?;
3149 assert_boolean_stats_min_max_error(kernel_predicated_stats_file_paths(
3150 &source,
3151 &col("is_current").not_eq(bool_lit(true)),
3152 ))?;
3153 assert_boolean_stats_min_max_error(kernel_predicated_stats_file_paths(
3154 &source,
3155 &col("is_current").not_eq(bool_lit(false)),
3156 ))?;
3157
3158 Ok(())
3159 }
3160
3161 #[test]
3162 fn kernel_boolean_data_column_stats_pruning_documents_null_checks()
3163 -> Result<(), Box<dyn std::error::Error>> {
3164 let (_table, source) = kernel_boolean_data_stats_characterization_source()?;
3165
3166 assert_eq!(
3167 kernel_predicated_stats_file_paths(&source, &col("is_current").is_null())?,
3168 vec![
3169 "boolean-all-null.parquet",
3170 "boolean-false-with-null.parquet",
3171 "boolean-missing-stats.parquet",
3172 "boolean-true-with-null.parquet",
3173 ]
3174 );
3175 assert_eq!(
3176 kernel_predicated_stats_file_paths(&source, &col("is_current").is_not_null())?,
3177 vec![
3178 "boolean-false-only.parquet",
3179 "boolean-false-with-null.parquet",
3180 "boolean-missing-stats.parquet",
3181 "boolean-mixed.parquet",
3182 "boolean-true-only.parquet",
3183 "boolean-true-with-null.parquet",
3184 ]
3185 );
3186
3187 Ok(())
3188 }
3189
3190 #[test]
3191 fn kernel_boolean_data_column_stats_pruning_keeps_partial_null_counts_uncertain()
3192 -> Result<(), Box<dyn std::error::Error>> {
3193 let (_table, source) = kernel_partial_boolean_data_stats_characterization_source()?;
3194
3195 assert_eq!(
3196 kernel_predicated_stats_file_paths(&source, &col("is_current").is_null())?,
3197 vec![
3198 "boolean-missing-null-count.parquet",
3199 "boolean-missing-stats.parquet",
3200 ]
3201 );
3202 assert_eq!(
3203 kernel_predicated_stats_file_paths(&source, &col("is_current").is_not_null())?,
3204 vec![
3205 "boolean-counts-only.parquet",
3206 "boolean-max-only-true.parquet",
3207 "boolean-min-only-false.parquet",
3208 "boolean-missing-null-count.parquet",
3209 "boolean-missing-stats.parquet",
3210 ]
3211 );
3212
3213 Ok(())
3214 }
3215
3216 #[test]
3217 fn kernel_date_data_column_stats_pruning_documents_comparisons()
3218 -> Result<(), Box<dyn std::error::Error>> {
3219 let (_table, source) = kernel_date_data_stats_characterization_source()?;
3220
3221 assert_eq!(
3222 kernel_predicated_stats_file_paths(&source, &col("event_date").gt(date_lit(19_782)))?,
3223 vec![
3224 "date-missing-stats.parquet",
3225 "date-new-year-only.parquet",
3226 "date-new-year-with-null.parquet",
3227 "date-range.parquet",
3228 ]
3229 );
3230 assert_eq!(
3231 kernel_predicated_stats_file_paths(
3232 &source,
3233 &col("event_date").gt_eq(date_lit(20_454)),
3234 )?,
3235 vec![
3236 "date-missing-stats.parquet",
3237 "date-new-year-only.parquet",
3238 "date-new-year-with-null.parquet",
3239 "date-range.parquet",
3240 ]
3241 );
3242 assert_eq!(
3243 kernel_predicated_stats_file_paths(&source, &col("event_date").lt_eq(date_lit(-1)),)?,
3244 vec!["date-missing-stats.parquet", "date-pre-epoch-only.parquet"]
3245 );
3246 assert_eq!(
3247 kernel_predicated_stats_file_paths(&source, &date_lit(20_454).gt(col("event_date")))?,
3248 vec![
3249 "date-leap-only.parquet",
3250 "date-missing-stats.parquet",
3251 "date-pre-epoch-only.parquet",
3252 "date-range.parquet",
3253 ]
3254 );
3255
3256 Ok(())
3257 }
3258
3259 #[test]
3260 fn kernel_date_data_column_stats_pruning_documents_equality_and_not_equals()
3261 -> Result<(), Box<dyn std::error::Error>> {
3262 let (_table, source) = kernel_date_data_stats_characterization_source()?;
3263
3264 assert_eq!(
3265 kernel_predicated_stats_file_paths(&source, &col("event_date").eq(date_lit(20_454)))?,
3266 vec![
3267 "date-missing-stats.parquet",
3268 "date-new-year-only.parquet",
3269 "date-new-year-with-null.parquet",
3270 "date-range.parquet",
3271 ]
3272 );
3273 assert_eq!(
3274 kernel_predicated_stats_file_paths(
3275 &source,
3276 &col("event_date").not_eq(date_lit(20_454)),
3277 )?,
3278 vec![
3279 "date-leap-only.parquet",
3280 "date-missing-stats.parquet",
3281 "date-pre-epoch-only.parquet",
3282 "date-range.parquet",
3283 ]
3284 );
3285
3286 Ok(())
3287 }
3288
3289 #[test]
3290 fn kernel_date_data_column_stats_pruning_documents_null_checks()
3291 -> Result<(), Box<dyn std::error::Error>> {
3292 let (_table, source) = kernel_date_data_stats_characterization_source()?;
3293
3294 assert_eq!(
3295 kernel_predicated_stats_file_paths(&source, &col("event_date").is_null())?,
3296 vec![
3297 "date-all-null.parquet",
3298 "date-missing-stats.parquet",
3299 "date-new-year-with-null.parquet",
3300 ]
3301 );
3302 assert_eq!(
3303 kernel_predicated_stats_file_paths(&source, &col("event_date").is_not_null())?,
3304 vec![
3305 "date-leap-only.parquet",
3306 "date-missing-stats.parquet",
3307 "date-new-year-only.parquet",
3308 "date-new-year-with-null.parquet",
3309 "date-pre-epoch-only.parquet",
3310 "date-range.parquet",
3311 ]
3312 );
3313
3314 Ok(())
3315 }
3316
3317 #[test]
3318 fn kernel_date_data_column_stats_pruning_documents_partial_stats_boundary()
3319 -> Result<(), Box<dyn std::error::Error>> {
3320 let (_table, source) = kernel_partial_date_data_stats_characterization_source()?;
3321
3322 assert_eq!(
3323 kernel_predicated_stats_file_paths(&source, &col("event_date").gt(date_lit(19_782)))?,
3324 vec![
3325 "date-counts-only.parquet",
3326 "date-min-only-high.parquet",
3327 "date-missing-null-count.parquet",
3328 "date-missing-stats.parquet",
3329 ]
3330 );
3331 assert_eq!(
3332 kernel_predicated_stats_file_paths(&source, &col("event_date").is_null())?,
3333 vec![
3334 "date-missing-null-count.parquet",
3335 "date-missing-stats.parquet",
3336 ]
3337 );
3338 assert_eq!(
3339 kernel_predicated_stats_file_paths(&source, &col("event_date").is_not_null())?,
3340 vec![
3341 "date-counts-only.parquet",
3342 "date-max-only-low.parquet",
3343 "date-min-only-high.parquet",
3344 "date-missing-null-count.parquet",
3345 "date-missing-stats.parquet",
3346 ]
3347 );
3348
3349 Ok(())
3350 }
3351
3352 #[test]
3353 fn kernel_decimal_data_column_stats_pruning_documents_comparisons()
3354 -> Result<(), Box<dyn std::error::Error>> {
3355 let (_table, source) = kernel_decimal_data_stats_characterization_source()?;
3356
3357 assert_eq!(
3358 kernel_predicated_stats_file_paths(&source, &col("amount").gt(decimal_lit(200)))?,
3359 vec![
3360 "decimal-large-only.parquet",
3361 "decimal-missing-stats.parquet",
3362 "decimal-range.parquet",
3363 "decimal-ten-only.parquet",
3364 ]
3365 );
3366 assert_eq!(
3367 kernel_predicated_stats_file_paths(&source, &col("amount").gt_eq(decimal_lit(1_000)))?,
3368 vec![
3369 "decimal-large-only.parquet",
3370 "decimal-missing-stats.parquet",
3371 "decimal-range.parquet",
3372 "decimal-ten-only.parquet",
3373 ]
3374 );
3375 assert_eq!(
3376 kernel_predicated_stats_file_paths(&source, &col("amount").lt_eq(decimal_lit(-123)))?,
3377 vec![
3378 "decimal-missing-stats.parquet",
3379 "decimal-negative-only.parquet",
3380 ]
3381 );
3382 assert_eq!(
3383 kernel_predicated_stats_file_paths(&source, &decimal_lit(1_000).gt(col("amount")))?,
3384 vec![
3385 "decimal-missing-stats.parquet",
3386 "decimal-negative-only.parquet",
3387 "decimal-range.parquet",
3388 "decimal-two-only.parquet",
3389 "decimal-two-with-null.parquet",
3390 "decimal-zero-only.parquet",
3391 ]
3392 );
3393
3394 Ok(())
3395 }
3396
3397 #[test]
3398 fn kernel_decimal_data_column_stats_pruning_documents_equality_and_not_equals()
3399 -> Result<(), Box<dyn std::error::Error>> {
3400 let (_table, source) = kernel_decimal_data_stats_characterization_source()?;
3401
3402 assert_eq!(
3403 kernel_predicated_stats_file_paths(&source, &col("amount").eq(decimal_lit(200)))?,
3404 vec![
3405 "decimal-missing-stats.parquet",
3406 "decimal-range.parquet",
3407 "decimal-two-only.parquet",
3408 "decimal-two-with-null.parquet",
3409 ]
3410 );
3411 assert_eq!(
3412 kernel_predicated_stats_file_paths(&source, &col("amount").not_eq(decimal_lit(200)))?,
3413 vec![
3414 "decimal-large-only.parquet",
3415 "decimal-missing-stats.parquet",
3416 "decimal-negative-only.parquet",
3417 "decimal-range.parquet",
3418 "decimal-ten-only.parquet",
3419 "decimal-zero-only.parquet",
3420 ]
3421 );
3422
3423 Ok(())
3424 }
3425
3426 #[test]
3427 fn kernel_decimal_data_column_stats_pruning_documents_null_checks()
3428 -> Result<(), Box<dyn std::error::Error>> {
3429 let (_table, source) = kernel_decimal_data_stats_characterization_source()?;
3430
3431 assert_eq!(
3432 kernel_predicated_stats_file_paths(&source, &col("amount").is_null())?,
3433 vec![
3434 "decimal-all-null.parquet",
3435 "decimal-missing-stats.parquet",
3436 "decimal-two-with-null.parquet",
3437 ]
3438 );
3439 assert_eq!(
3440 kernel_predicated_stats_file_paths(&source, &col("amount").is_not_null())?,
3441 vec![
3442 "decimal-large-only.parquet",
3443 "decimal-missing-stats.parquet",
3444 "decimal-negative-only.parquet",
3445 "decimal-range.parquet",
3446 "decimal-ten-only.parquet",
3447 "decimal-two-only.parquet",
3448 "decimal-two-with-null.parquet",
3449 "decimal-zero-only.parquet",
3450 ]
3451 );
3452
3453 Ok(())
3454 }
3455
3456 #[test]
3457 fn kernel_decimal_data_column_stats_pruning_documents_partial_stats_boundary()
3458 -> Result<(), Box<dyn std::error::Error>> {
3459 let (_table, source) = kernel_partial_decimal_data_stats_characterization_source()?;
3460
3461 assert_eq!(
3462 kernel_predicated_stats_file_paths(&source, &col("amount").gt(decimal_lit(200)))?,
3463 vec![
3464 "decimal-counts-only.parquet",
3465 "decimal-min-only-high.parquet",
3466 "decimal-missing-null-count.parquet",
3467 "decimal-missing-stats.parquet",
3468 ]
3469 );
3470 assert_eq!(
3471 kernel_predicated_stats_file_paths(&source, &col("amount").is_null())?,
3472 vec![
3473 "decimal-missing-null-count.parquet",
3474 "decimal-missing-stats.parquet",
3475 ]
3476 );
3477 assert_eq!(
3478 kernel_predicated_stats_file_paths(&source, &col("amount").is_not_null())?,
3479 vec![
3480 "decimal-counts-only.parquet",
3481 "decimal-max-only-low.parquet",
3482 "decimal-min-only-high.parquet",
3483 "decimal-missing-null-count.parquet",
3484 "decimal-missing-stats.parquet",
3485 ]
3486 );
3487
3488 Ok(())
3489 }
3490
3491 #[test]
3492 fn kernel_decimal_data_column_stats_pruning_documents_precision_and_scale_boundary()
3493 -> Result<(), Box<dyn std::error::Error>> {
3494 let (_table, source) = kernel_decimal_data_stats_characterization_source()?;
3495
3496 for (description, literal) in [
3497 ("scale mismatch", decimal_lit_with_type(2_000, 11, 3)),
3498 ("precision mismatch", decimal_lit_with_type(200, 11, 2)),
3499 ] {
3500 let error = kernel_predicated_stats_file_paths(&source, &col("amount").eq(literal))
3501 .err()
3502 .ok_or_else(|| {
3503 format!("{description} decimal predicate should fail kernel scan")
3504 })?;
3505 let message = error.to_string();
3506 let debug_message = format!("{error:?}");
3507 assert!(
3508 message.contains("Invalid comparison operation")
3509 || debug_message.contains("Invalid comparison operation"),
3510 "{message}\n{debug_message}"
3511 );
3512 }
3513
3514 Ok(())
3515 }
3516
3517 #[test]
3518 fn kernel_binary_data_column_stats_pruning_documents_equality_and_empty_boundary()
3519 -> Result<(), Box<dyn std::error::Error>> {
3520 let (_table, source) = kernel_binary_data_stats_characterization_source()?;
3521
3522 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3523 &source,
3524 &col("payload").eq(binary_lit(b"hello")),
3525 ))?;
3526 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3527 &source,
3528 &col("payload").not_eq(binary_lit(b"hello")),
3529 ))?;
3530 assert_unsupported_literal_error(kernel_predicated_stats_file_paths(
3531 &source,
3532 &col("payload").eq(binary_lit(b"")),
3533 ))?;
3534 assert_unsupported_literal_error(kernel_predicated_stats_file_paths(
3535 &source,
3536 &col("payload").not_eq(binary_lit(b"")),
3537 ))?;
3538
3539 Ok(())
3540 }
3541
3542 #[test]
3543 fn kernel_binary_data_column_stats_pruning_documents_ordering()
3544 -> Result<(), Box<dyn std::error::Error>> {
3545 let (_table, source) = kernel_binary_data_stats_characterization_source()?;
3546
3547 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3548 &source,
3549 &col("payload").gt(binary_lit(b"hello")),
3550 ))?;
3551 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3552 &source,
3553 &col("payload").lt(binary_lit(b"hello")),
3554 ))?;
3555 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3556 &source,
3557 &binary_lit(b"hello").gt(col("payload")),
3558 ))?;
3559
3560 Ok(())
3561 }
3562
3563 #[test]
3564 fn kernel_binary_data_column_stats_pruning_documents_null_checks()
3565 -> Result<(), Box<dyn std::error::Error>> {
3566 let (_table, source) = kernel_binary_data_stats_characterization_source()?;
3567
3568 assert_eq!(
3569 kernel_predicated_stats_file_paths(&source, &col("payload").is_null())?,
3570 vec![
3571 "binary-all-null.parquet",
3572 "binary-missing-stats.parquet",
3573 "binary-with-null.parquet",
3574 ]
3575 );
3576 assert_eq!(
3577 kernel_predicated_stats_file_paths(&source, &col("payload").is_not_null())?,
3578 vec![
3579 "binary-HELLO.parquet",
3580 "binary-empty.parquet",
3581 "binary-hello.parquet",
3582 "binary-missing-stats.parquet",
3583 "binary-range.parquet",
3584 "binary-special.parquet",
3585 "binary-with-null.parquet",
3586 ]
3587 );
3588
3589 Ok(())
3590 }
3591
3592 #[test]
3593 fn kernel_binary_data_column_stats_pruning_documents_partial_stats_boundary()
3594 -> Result<(), Box<dyn std::error::Error>> {
3595 let (_table, source) = kernel_partial_binary_data_stats_characterization_source()?;
3596
3597 assert_binary_stats_min_max_error(kernel_predicated_stats_file_paths(
3598 &source,
3599 &col("payload").gt(binary_lit(b"hello")),
3600 ))?;
3601 assert_eq!(
3602 kernel_predicated_stats_file_paths(&source, &col("payload").is_null())?,
3603 vec![
3604 "binary-missing-null-count.parquet",
3605 "binary-missing-stats.parquet",
3606 ]
3607 );
3608 assert_eq!(
3609 kernel_predicated_stats_file_paths(&source, &col("payload").is_not_null())?,
3610 vec![
3611 "binary-counts-only.parquet",
3612 "binary-max-only-low.parquet",
3613 "binary-min-only-high.parquet",
3614 "binary-missing-null-count.parquet",
3615 "binary-missing-stats.parquet",
3616 ]
3617 );
3618
3619 Ok(())
3620 }
3621
3622 #[test]
3623 fn kernel_floating_data_column_stats_pruning_documents_comparisons()
3624 -> Result<(), Box<dyn std::error::Error>> {
3625 let (_table, source) = kernel_floating_data_stats_characterization_source()?;
3626
3627 assert_eq!(
3628 kernel_predicated_stats_file_paths(&source, &col("float_score").gt(float32_lit(1.5)))?,
3629 vec![
3630 "floating-missing-stats.parquet",
3631 "floating-range.parquet",
3632 "floating-ten.parquet",
3633 ]
3634 );
3635 assert_eq!(
3636 kernel_predicated_stats_file_paths(&source, &col("double_score").lt(float64_lit(0.0)),)?,
3637 vec![
3638 "floating-missing-stats.parquet",
3639 "floating-neg-zero.parquet",
3640 "floating-neg.parquet",
3641 "floating-range.parquet",
3642 ]
3643 );
3644 assert_eq!(
3645 kernel_predicated_stats_file_paths(&source, &float64_lit(0.0).gt(col("double_score")))?,
3646 vec![
3647 "floating-missing-stats.parquet",
3648 "floating-neg-zero.parquet",
3649 "floating-neg.parquet",
3650 "floating-range.parquet",
3651 ]
3652 );
3653 assert_eq!(
3654 kernel_predicated_stats_file_paths(&source, &col("float_score").lt(float32_lit(10.0)))?,
3655 vec![
3656 "floating-missing-stats.parquet",
3657 "floating-neg-zero.parquet",
3658 "floating-neg.parquet",
3659 "floating-one-with-null.parquet",
3660 "floating-one.parquet",
3661 "floating-pos-zero.parquet",
3662 "floating-range.parquet",
3663 ]
3664 );
3665
3666 Ok(())
3667 }
3668
3669 #[test]
3670 fn kernel_floating_data_column_stats_pruning_documents_equality_and_signed_zero()
3671 -> Result<(), Box<dyn std::error::Error>> {
3672 let (_table, source) = kernel_floating_data_stats_characterization_source()?;
3673
3674 assert_eq!(
3675 kernel_predicated_stats_file_paths(&source, &col("float_score").eq(float32_lit(1.5)))?,
3676 vec![
3677 "floating-missing-stats.parquet",
3678 "floating-one-with-null.parquet",
3679 "floating-one.parquet",
3680 "floating-range.parquet",
3681 ]
3682 );
3683 assert_eq!(
3684 kernel_predicated_stats_file_paths(
3685 &source,
3686 &col("float_score").not_eq(float32_lit(1.5)),
3687 )?,
3688 vec![
3689 "floating-missing-stats.parquet",
3690 "floating-neg-zero.parquet",
3691 "floating-neg.parquet",
3692 "floating-pos-zero.parquet",
3693 "floating-range.parquet",
3694 "floating-ten.parquet",
3695 ]
3696 );
3697 assert_eq!(
3698 kernel_predicated_stats_file_paths(&source, &col("float_score").eq(float32_lit(-0.0)))?,
3699 vec![
3700 "floating-missing-stats.parquet",
3701 "floating-neg-zero.parquet",
3702 "floating-range.parquet",
3703 ]
3704 );
3705 assert_eq!(
3706 kernel_predicated_stats_file_paths(&source, &col("float_score").eq(float32_lit(0.0)))?,
3707 vec![
3708 "floating-missing-stats.parquet",
3709 "floating-pos-zero.parquet",
3710 "floating-range.parquet",
3711 ]
3712 );
3713
3714 Ok(())
3715 }
3716
3717 #[test]
3718 fn kernel_floating_data_column_stats_pruning_documents_signed_zero_operator_boundary()
3719 -> Result<(), Box<dyn std::error::Error>> {
3720 let (_table, source) = kernel_floating_data_stats_characterization_source()?;
3721
3722 assert_eq!(
3723 kernel_predicated_stats_file_paths(&source, &col("float_score").lt(float32_lit(0.0)))?,
3724 vec![
3725 "floating-missing-stats.parquet",
3726 "floating-neg-zero.parquet",
3727 "floating-neg.parquet",
3728 "floating-range.parquet",
3729 ]
3730 );
3731 assert_eq!(
3732 kernel_predicated_stats_file_paths(
3733 &source,
3734 &col("float_score").lt_eq(float32_lit(0.0)),
3735 )?,
3736 vec![
3737 "floating-missing-stats.parquet",
3738 "floating-neg-zero.parquet",
3739 "floating-neg.parquet",
3740 "floating-pos-zero.parquet",
3741 "floating-range.parquet",
3742 ]
3743 );
3744 assert_eq!(
3745 kernel_predicated_stats_file_paths(&source, &col("float_score").gt(float32_lit(0.0)))?,
3746 vec![
3747 "floating-missing-stats.parquet",
3748 "floating-one-with-null.parquet",
3749 "floating-one.parquet",
3750 "floating-range.parquet",
3751 "floating-ten.parquet",
3752 ]
3753 );
3754 assert_eq!(
3755 kernel_predicated_stats_file_paths(
3756 &source,
3757 &col("float_score").gt_eq(float32_lit(0.0)),
3758 )?,
3759 vec![
3760 "floating-missing-stats.parquet",
3761 "floating-one-with-null.parquet",
3762 "floating-one.parquet",
3763 "floating-pos-zero.parquet",
3764 "floating-range.parquet",
3765 "floating-ten.parquet",
3766 ]
3767 );
3768 assert_eq!(
3769 kernel_predicated_stats_file_paths(
3770 &source,
3771 &col("float_score").not_eq(float32_lit(0.0)),
3772 )?,
3773 vec![
3774 "floating-missing-stats.parquet",
3775 "floating-neg-zero.parquet",
3776 "floating-neg.parquet",
3777 "floating-one-with-null.parquet",
3778 "floating-one.parquet",
3779 "floating-range.parquet",
3780 "floating-ten.parquet",
3781 ]
3782 );
3783
3784 Ok(())
3785 }
3786
3787 #[test]
3788 fn kernel_floating_data_column_stats_pruning_documents_null_checks()
3789 -> Result<(), Box<dyn std::error::Error>> {
3790 let (_table, source) = kernel_floating_data_stats_characterization_source()?;
3791
3792 assert_eq!(
3793 kernel_predicated_stats_file_paths(&source, &col("float_score").is_null())?,
3794 vec![
3795 "floating-all-null.parquet",
3796 "floating-missing-stats.parquet",
3797 "floating-one-with-null.parquet",
3798 ]
3799 );
3800 assert_eq!(
3801 kernel_predicated_stats_file_paths(&source, &col("double_score").is_not_null())?,
3802 vec![
3803 "floating-missing-stats.parquet",
3804 "floating-neg-zero.parquet",
3805 "floating-neg.parquet",
3806 "floating-one-with-null.parquet",
3807 "floating-one.parquet",
3808 "floating-pos-zero.parquet",
3809 "floating-range.parquet",
3810 "floating-ten.parquet",
3811 ]
3812 );
3813
3814 Ok(())
3815 }
3816
3817 #[test]
3818 fn kernel_floating_data_column_stats_pruning_documents_partial_stats_boundary()
3819 -> Result<(), Box<dyn std::error::Error>> {
3820 let (_table, source) = kernel_partial_floating_data_stats_characterization_source()?;
3821
3822 assert_eq!(
3823 kernel_predicated_stats_file_paths(&source, &col("float_score").gt(float32_lit(1.0)))?,
3824 vec![
3825 "floating-counts-only.parquet",
3826 "floating-min-only-high.parquet",
3827 "floating-missing-null-count.parquet",
3828 "floating-missing-stats.parquet",
3829 ]
3830 );
3831 assert_eq!(
3832 kernel_predicated_stats_file_paths(&source, &col("float_score").is_null())?,
3833 vec![
3834 "floating-missing-null-count.parquet",
3835 "floating-missing-stats.parquet",
3836 ]
3837 );
3838 assert_eq!(
3839 kernel_predicated_stats_file_paths(&source, &col("float_score").is_not_null())?,
3840 vec![
3841 "floating-counts-only.parquet",
3842 "floating-max-only-low.parquet",
3843 "floating-min-only-high.parquet",
3844 "floating-missing-null-count.parquet",
3845 "floating-missing-stats.parquet",
3846 ]
3847 );
3848
3849 Ok(())
3850 }
3851
3852 #[test]
3853 fn kernel_floating_data_column_stats_pruning_documents_nonfinite_stats_boundary()
3854 -> Result<(), Box<dyn std::error::Error>> {
3855 let (_table, source) = kernel_nonfinite_floating_data_stats_characterization_source()?;
3856
3857 assert_eq!(
3858 kernel_predicated_stats_file_paths(&source, &col("float_score").gt(float32_lit(1.0)))?,
3859 vec![
3860 "floating-inf.parquet",
3861 "floating-missing-stats.parquet",
3862 "floating-nan.parquet",
3863 "floating-valid.parquet",
3864 ]
3865 );
3866 assert_eq!(
3867 kernel_predicated_stats_file_paths(&source, &col("double_score").lt(float64_lit(0.0)))?,
3868 vec!["floating-missing-stats.parquet", "floating-neg-inf.parquet"]
3869 );
3870 assert_eq!(
3871 kernel_predicated_stats_file_paths(&source, &col("float_score").eq(float32_lit(1.5)),)?,
3872 vec!["floating-missing-stats.parquet", "floating-valid.parquet"]
3873 );
3874 assert_eq!(
3875 kernel_predicated_stats_file_paths(
3876 &source,
3877 &col("float_score").not_eq(float32_lit(1.5)),
3878 )?,
3879 vec![
3880 "floating-inf.parquet",
3881 "floating-missing-stats.parquet",
3882 "floating-nan.parquet",
3883 "floating-neg-inf.parquet",
3884 ]
3885 );
3886
3887 Ok(())
3888 }
3889
3890 #[test]
3891 fn kernel_string_data_column_stats_pruning_documents_comparisons()
3892 -> Result<(), Box<dyn std::error::Error>> {
3893 let (_table, source) = kernel_string_data_stats_characterization_source()?;
3894
3895 assert_eq!(
3896 kernel_predicated_stats_file_paths(&source, &col("customer_name").gt(string_lit("m")),)?,
3897 vec![
3898 "string-missing-stats.parquet",
3899 "string-range.parquet",
3900 "string-zed-only.parquet",
3901 ]
3902 );
3903 assert_eq!(
3904 kernel_predicated_stats_file_paths(
3905 &source,
3906 &col("customer_name").gt_eq(string_lit("morgan")),
3907 )?,
3908 vec![
3909 "string-missing-stats.parquet",
3910 "string-range.parquet",
3911 "string-zed-only.parquet",
3912 ]
3913 );
3914 assert_eq!(
3915 kernel_predicated_stats_file_paths(
3916 &source,
3917 &col("customer_name").lt_eq(string_lit("Alice")),
3918 )?,
3919 vec![
3920 "string-empty-only.parquet",
3921 "string-missing-stats.parquet",
3922 "string-mixed-case-only.parquet",
3923 ]
3924 );
3925 assert_eq!(
3926 kernel_predicated_stats_file_paths(&source, &string_lit("m").gt(col("customer_name")),)?,
3927 vec![
3928 "string-alice-only.parquet",
3929 "string-alice-with-null.parquet",
3930 "string-bob-only.parquet",
3931 "string-empty-only.parquet",
3932 "string-missing-stats.parquet",
3933 "string-mixed-case-only.parquet",
3934 "string-range.parquet",
3935 ]
3936 );
3937
3938 Ok(())
3939 }
3940
3941 #[test]
3942 fn kernel_string_data_column_stats_pruning_documents_equality_and_not_equals()
3943 -> Result<(), Box<dyn std::error::Error>> {
3944 let (_table, source) = kernel_string_data_stats_characterization_source()?;
3945
3946 assert_eq!(
3947 kernel_predicated_stats_file_paths(
3948 &source,
3949 &col("customer_name").eq(string_lit("alice")),
3950 )?,
3951 vec![
3952 "string-alice-only.parquet",
3953 "string-alice-with-null.parquet",
3954 "string-missing-stats.parquet",
3955 "string-range.parquet",
3956 ]
3957 );
3958 assert_eq!(
3959 kernel_predicated_stats_file_paths(
3960 &source,
3961 &col("customer_name").not_eq(string_lit("alice")),
3962 )?,
3963 vec![
3964 "string-bob-only.parquet",
3965 "string-empty-only.parquet",
3966 "string-missing-stats.parquet",
3967 "string-mixed-case-only.parquet",
3968 "string-range.parquet",
3969 "string-zed-only.parquet",
3970 ]
3971 );
3972
3973 Ok(())
3974 }
3975
3976 #[test]
3977 fn kernel_string_data_column_stats_pruning_documents_null_checks()
3978 -> Result<(), Box<dyn std::error::Error>> {
3979 let (_table, source) = kernel_string_data_stats_characterization_source()?;
3980
3981 assert_eq!(
3982 kernel_predicated_stats_file_paths(&source, &col("customer_name").is_null())?,
3983 vec![
3984 "string-alice-with-null.parquet",
3985 "string-all-null.parquet",
3986 "string-missing-stats.parquet",
3987 ]
3988 );
3989 assert_eq!(
3990 kernel_predicated_stats_file_paths(&source, &col("customer_name").is_not_null())?,
3991 vec![
3992 "string-alice-only.parquet",
3993 "string-alice-with-null.parquet",
3994 "string-bob-only.parquet",
3995 "string-empty-only.parquet",
3996 "string-missing-stats.parquet",
3997 "string-mixed-case-only.parquet",
3998 "string-range.parquet",
3999 "string-zed-only.parquet",
4000 ]
4001 );
4002
4003 Ok(())
4004 }
4005
4006 #[test]
4007 fn kernel_string_data_column_stats_pruning_documents_partial_stats_boundary()
4008 -> Result<(), Box<dyn std::error::Error>> {
4009 let (_table, source) = kernel_partial_string_data_stats_characterization_source()?;
4010
4011 assert_eq!(
4012 kernel_predicated_stats_file_paths(&source, &col("customer_name").gt(string_lit("m")),)?,
4013 vec![
4014 "string-counts-only.parquet",
4015 "string-min-only-morgan.parquet",
4016 "string-missing-null-count.parquet",
4017 "string-missing-stats.parquet",
4018 ]
4019 );
4020 assert_eq!(
4021 kernel_predicated_stats_file_paths(&source, &col("customer_name").is_null())?,
4022 vec![
4023 "string-missing-null-count.parquet",
4024 "string-missing-stats.parquet",
4025 ]
4026 );
4027 assert_eq!(
4028 kernel_predicated_stats_file_paths(&source, &col("customer_name").is_not_null())?,
4029 vec![
4030 "string-counts-only.parquet",
4031 "string-max-only-alice.parquet",
4032 "string-min-only-morgan.parquet",
4033 "string-missing-null-count.parquet",
4034 "string-missing-stats.parquet",
4035 ]
4036 );
4037
4038 Ok(())
4039 }
4040
4041 #[test]
4042 fn kernel_string_data_column_stats_pruning_documents_large_utf8_literal_boundary()
4043 -> Result<(), Box<dyn std::error::Error>> {
4044 let (_table, source) = kernel_string_data_stats_characterization_source()?;
4045
4046 assert_eq!(
4047 kernel_predicated_stats_file_paths(
4048 &source,
4049 &col("customer_name").eq(large_string_lit("alice")),
4050 )?,
4051 vec![
4052 "string-alice-only.parquet",
4053 "string-alice-with-null.parquet",
4054 "string-missing-stats.parquet",
4055 "string-range.parquet",
4056 ]
4057 );
4058 assert_eq!(
4059 kernel_predicated_stats_file_paths(
4060 &source,
4061 &col("customer_name").gt(large_string_lit("m")),
4062 )?,
4063 vec![
4064 "string-missing-stats.parquet",
4065 "string-range.parquet",
4066 "string-zed-only.parquet",
4067 ]
4068 );
4069
4070 Ok(())
4071 }
4072
4073 #[test]
4074 fn kernel_string_data_column_stats_pruning_documents_non_ascii_ordering()
4075 -> Result<(), Box<dyn std::error::Error>> {
4076 let (_table, source) = kernel_non_ascii_string_data_stats_characterization_source()?;
4077 let eclair = string_lit("\u{00e9}clair");
4078
4079 assert_eq!(
4080 kernel_predicated_stats_file_paths(&source, &col("customer_name").eq(eclair.clone()),)?,
4081 vec!["string-eclair.parquet", "string-missing-stats.parquet"]
4082 );
4083 assert_eq!(
4084 kernel_predicated_stats_file_paths(
4085 &source,
4086 &col("customer_name").eq(large_string_lit("\u{00e9}clair")),
4087 )?,
4088 vec!["string-eclair.parquet", "string-missing-stats.parquet"]
4089 );
4090 assert_eq!(
4091 kernel_predicated_stats_file_paths(
4092 &source,
4093 &col("customer_name").gt_eq(eclair.clone()),
4094 )?,
4095 vec![
4096 "string-eclair.parquet",
4097 "string-emile.parquet",
4098 "string-missing-stats.parquet",
4099 ]
4100 );
4101 assert_eq!(
4102 kernel_predicated_stats_file_paths(&source, &col("customer_name").lt(eclair.clone()),)?,
4103 vec![
4104 "string-ascii-cafe.parquet",
4105 "string-ascii-zulu.parquet",
4106 "string-missing-stats.parquet",
4107 ]
4108 );
4109 assert_eq!(
4110 kernel_predicated_stats_file_paths(&source, &col("customer_name").gt(eclair),)?,
4111 vec!["string-emile.parquet", "string-missing-stats.parquet"]
4112 );
4113
4114 Ok(())
4115 }
4116
4117 #[test]
4118 fn kernel_timestamp_data_column_stats_pruning_documents_comparisons()
4119 -> Result<(), Box<dyn std::error::Error>> {
4120 let (_table, source) = kernel_timestamp_data_stats_characterization_source()?;
4121 let low = 1_767_225_599_999_999_i64;
4122 let target = 1_767_225_600_123_456_i64;
4123 let high = 1_767_225_600_123_457_i64;
4124
4125 assert_eq!(
4126 kernel_predicated_stats_file_paths(
4127 &source,
4128 &col("event_ts").lt(timestamp_lit(target, "UTC")),
4129 )?,
4130 vec![
4131 "timestamp-low-only.parquet",
4132 "timestamp-missing-stats.parquet",
4133 "timestamp-pre-epoch-only.parquet",
4134 "timestamp-range.parquet",
4135 ]
4136 );
4137 assert_eq!(
4138 kernel_predicated_stats_file_paths(
4139 &source,
4140 &col("event_ts").gt_eq(timestamp_lit(target, "UTC")),
4141 )?,
4142 vec![
4143 "timestamp-high-only.parquet",
4144 "timestamp-missing-stats.parquet",
4145 "timestamp-range.parquet",
4146 "timestamp-target-only.parquet",
4147 "timestamp-target-with-null.parquet",
4148 ]
4149 );
4150 assert_eq!(
4151 kernel_predicated_stats_file_paths(
4152 &source,
4153 ×tamp_lit(high, "UTC").gt(col("event_ts")),
4154 )?,
4155 vec![
4156 "timestamp-low-only.parquet",
4157 "timestamp-missing-stats.parquet",
4158 "timestamp-pre-epoch-only.parquet",
4159 "timestamp-range.parquet",
4160 "timestamp-target-only.parquet",
4161 "timestamp-target-with-null.parquet",
4162 ]
4163 );
4164 assert_eq!(
4165 kernel_predicated_stats_file_paths(
4166 &source,
4167 &col("event_ts").eq(timestamp_lit(low, "UTC")),
4168 )?,
4169 vec![
4170 "timestamp-low-only.parquet",
4171 "timestamp-missing-stats.parquet",
4172 "timestamp-range.parquet",
4173 ]
4174 );
4175 assert_eq!(
4176 kernel_predicated_stats_file_paths(
4177 &source,
4178 &col("event_ts").not_eq(timestamp_lit(target, "UTC")),
4179 )?,
4180 vec![
4181 "timestamp-high-only.parquet",
4182 "timestamp-low-only.parquet",
4183 "timestamp-missing-stats.parquet",
4184 "timestamp-pre-epoch-only.parquet",
4185 "timestamp-range.parquet",
4186 "timestamp-target-only.parquet",
4187 "timestamp-target-with-null.parquet",
4188 ]
4189 );
4190
4191 Ok(())
4192 }
4193
4194 #[test]
4195 fn kernel_timestamp_data_column_stats_pruning_documents_null_checks()
4196 -> Result<(), Box<dyn std::error::Error>> {
4197 let (_table, source) = kernel_timestamp_data_stats_characterization_source()?;
4198
4199 assert_eq!(
4200 kernel_predicated_stats_file_paths(&source, &col("event_ts").is_null())?,
4201 vec![
4202 "timestamp-all-null.parquet",
4203 "timestamp-missing-stats.parquet",
4204 "timestamp-target-with-null.parquet",
4205 ]
4206 );
4207 assert_eq!(
4208 kernel_predicated_stats_file_paths(&source, &col("event_ts").is_not_null())?,
4209 vec![
4210 "timestamp-high-only.parquet",
4211 "timestamp-low-only.parquet",
4212 "timestamp-missing-stats.parquet",
4213 "timestamp-pre-epoch-only.parquet",
4214 "timestamp-range.parquet",
4215 "timestamp-target-only.parquet",
4216 "timestamp-target-with-null.parquet",
4217 ]
4218 );
4219
4220 Ok(())
4221 }
4222
4223 #[test]
4224 fn kernel_timestamp_data_column_stats_pruning_documents_partial_stats_boundary()
4225 -> Result<(), Box<dyn std::error::Error>> {
4226 let (_table, source) = kernel_partial_timestamp_data_stats_characterization_source()?;
4227 let low = 1_767_225_599_999_999_i64;
4228
4229 assert_eq!(
4230 kernel_predicated_stats_file_paths(
4231 &source,
4232 &col("event_ts").gt(timestamp_lit(low, "UTC")),
4233 )?,
4234 vec![
4235 "timestamp-counts-only.parquet",
4236 "timestamp-max-only-low.parquet",
4237 "timestamp-min-only-target.parquet",
4238 "timestamp-missing-null-count.parquet",
4239 "timestamp-missing-stats.parquet",
4240 ]
4241 );
4242 assert_eq!(
4243 kernel_predicated_stats_file_paths(&source, &col("event_ts").is_null())?,
4244 vec![
4245 "timestamp-missing-null-count.parquet",
4246 "timestamp-missing-stats.parquet",
4247 ]
4248 );
4249 assert_eq!(
4250 kernel_predicated_stats_file_paths(&source, &col("event_ts").is_not_null())?,
4251 vec![
4252 "timestamp-counts-only.parquet",
4253 "timestamp-max-only-low.parquet",
4254 "timestamp-min-only-target.parquet",
4255 "timestamp-missing-null-count.parquet",
4256 "timestamp-missing-stats.parquet",
4257 ]
4258 );
4259
4260 Ok(())
4261 }
4262
4263 #[test]
4264 fn kernel_timestamp_ntz_data_column_stats_pruning_documents_comparisons()
4265 -> Result<(), Box<dyn std::error::Error>> {
4266 let (_table, source) = kernel_timestamp_ntz_data_stats_characterization_source()?;
4267 let low = 1_767_225_599_999_999_i64;
4268 let target = 1_767_225_600_123_456_i64;
4269 let high = 1_767_225_600_123_457_i64;
4270
4271 assert_eq!(
4272 kernel_predicated_stats_file_paths(
4273 &source,
4274 &col("event_ts_ntz").lt(timestamp_ntz_lit(target)),
4275 )?,
4276 vec![
4277 "timestamp-ntz-low-only.parquet",
4278 "timestamp-ntz-missing-stats.parquet",
4279 "timestamp-ntz-pre-epoch-only.parquet",
4280 "timestamp-ntz-range.parquet",
4281 ]
4282 );
4283 assert_eq!(
4284 kernel_predicated_stats_file_paths(
4285 &source,
4286 &col("event_ts_ntz").gt_eq(timestamp_ntz_lit(target)),
4287 )?,
4288 vec![
4289 "timestamp-ntz-high-only.parquet",
4290 "timestamp-ntz-missing-stats.parquet",
4291 "timestamp-ntz-range.parquet",
4292 "timestamp-ntz-target-only.parquet",
4293 "timestamp-ntz-target-with-null.parquet",
4294 ]
4295 );
4296 assert_eq!(
4297 kernel_predicated_stats_file_paths(
4298 &source,
4299 ×tamp_ntz_lit(high).gt(col("event_ts_ntz")),
4300 )?,
4301 vec![
4302 "timestamp-ntz-low-only.parquet",
4303 "timestamp-ntz-missing-stats.parquet",
4304 "timestamp-ntz-pre-epoch-only.parquet",
4305 "timestamp-ntz-range.parquet",
4306 "timestamp-ntz-target-only.parquet",
4307 "timestamp-ntz-target-with-null.parquet",
4308 ]
4309 );
4310 assert_eq!(
4311 kernel_predicated_stats_file_paths(
4312 &source,
4313 &col("event_ts_ntz").eq(timestamp_ntz_lit(low)),
4314 )?,
4315 vec![
4316 "timestamp-ntz-low-only.parquet",
4317 "timestamp-ntz-missing-stats.parquet",
4318 "timestamp-ntz-range.parquet",
4319 ]
4320 );
4321 assert_eq!(
4322 kernel_predicated_stats_file_paths(
4323 &source,
4324 &col("event_ts_ntz").not_eq(timestamp_ntz_lit(target)),
4325 )?,
4326 vec![
4327 "timestamp-ntz-high-only.parquet",
4328 "timestamp-ntz-low-only.parquet",
4329 "timestamp-ntz-missing-stats.parquet",
4330 "timestamp-ntz-pre-epoch-only.parquet",
4331 "timestamp-ntz-range.parquet",
4332 "timestamp-ntz-target-only.parquet",
4333 "timestamp-ntz-target-with-null.parquet",
4334 ]
4335 );
4336
4337 Ok(())
4338 }
4339
4340 #[test]
4341 fn kernel_timestamp_ntz_data_column_stats_pruning_documents_null_checks()
4342 -> Result<(), Box<dyn std::error::Error>> {
4343 let (_table, source) = kernel_timestamp_ntz_data_stats_characterization_source()?;
4344
4345 assert_eq!(
4346 kernel_predicated_stats_file_paths(&source, &col("event_ts_ntz").is_null())?,
4347 vec![
4348 "timestamp-ntz-all-null.parquet",
4349 "timestamp-ntz-missing-stats.parquet",
4350 "timestamp-ntz-target-with-null.parquet",
4351 ]
4352 );
4353 assert_eq!(
4354 kernel_predicated_stats_file_paths(&source, &col("event_ts_ntz").is_not_null())?,
4355 vec![
4356 "timestamp-ntz-high-only.parquet",
4357 "timestamp-ntz-low-only.parquet",
4358 "timestamp-ntz-missing-stats.parquet",
4359 "timestamp-ntz-pre-epoch-only.parquet",
4360 "timestamp-ntz-range.parquet",
4361 "timestamp-ntz-target-only.parquet",
4362 "timestamp-ntz-target-with-null.parquet",
4363 ]
4364 );
4365
4366 Ok(())
4367 }
4368
4369 #[test]
4370 fn kernel_timestamp_ntz_data_column_stats_pruning_documents_partial_stats_boundary()
4371 -> Result<(), Box<dyn std::error::Error>> {
4372 let (_table, source) = kernel_partial_timestamp_ntz_data_stats_characterization_source()?;
4373 let low = 1_767_225_599_999_999_i64;
4374
4375 assert_eq!(
4376 kernel_predicated_stats_file_paths(
4377 &source,
4378 &col("event_ts_ntz").gt(timestamp_ntz_lit(low)),
4379 )?,
4380 vec![
4381 "timestamp-ntz-counts-only.parquet",
4382 "timestamp-ntz-max-only-low.parquet",
4383 "timestamp-ntz-min-only-target.parquet",
4384 "timestamp-ntz-missing-null-count.parquet",
4385 "timestamp-ntz-missing-stats.parquet",
4386 ]
4387 );
4388 assert_eq!(
4389 kernel_predicated_stats_file_paths(&source, &col("event_ts_ntz").is_null())?,
4390 vec![
4391 "timestamp-ntz-missing-null-count.parquet",
4392 "timestamp-ntz-missing-stats.parquet",
4393 ]
4394 );
4395 assert_eq!(
4396 kernel_predicated_stats_file_paths(&source, &col("event_ts_ntz").is_not_null())?,
4397 vec![
4398 "timestamp-ntz-counts-only.parquet",
4399 "timestamp-ntz-max-only-low.parquet",
4400 "timestamp-ntz-min-only-target.parquet",
4401 "timestamp-ntz-missing-null-count.parquet",
4402 "timestamp-ntz-missing-stats.parquet",
4403 ]
4404 );
4405
4406 Ok(())
4407 }
4408
4409 #[test]
4410 fn kernel_predicated_stats_scan_exposes_stats_for_surviving_files()
4411 -> Result<(), Box<dyn std::error::Error>> {
4412 let (_table, source) = kernel_data_stats_characterization_source()?;
4413 let predicate = datafusion_expr_to_kernel_predicate(&col("id").gt(int32_lit(100)))?;
4414 let stats_scan = build_projected_predicated_stats_delta_scan(&source, Some(predicate))?;
4415
4416 assert_eq!(
4417 kernel_scan_file_paths(&stats_scan, source.table_uri())?,
4418 vec!["id-missing-stats.parquet", "id-possible.parquet"]
4419 );
4420 assert_eq!(
4421 kernel_stats_metadata_boundary(&stats_scan, source.table_uri())?,
4422 KernelStatsMetadataBoundary {
4423 selected_rows: 2,
4424 selected_stats_rows: 1,
4425 selected_missing_stats_rows: 1,
4426 has_id_min_values: true,
4427 has_id_max_values: true,
4428 has_id_null_count: true,
4429 }
4430 );
4431
4432 Ok(())
4433 }
4434
4435 #[test]
4436 fn kernel_stats_pruning_preserves_surviving_dv_metadata_boundary()
4437 -> Result<(), Box<dyn std::error::Error>> {
4438 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
4439 "kernel-dv-data-stats-boundary",
4440 DELETION_VECTOR_PROTOCOL_JSON,
4441 METADATA_JSON,
4442 &[
4443 dv_add_json_with_id_stats("id-dv-impossible.parquet", 10, 1, 50, 0),
4444 dv_add_json_with_id_stats("id-dv-possible.parquet", 10, 101, 150, 0),
4445 add_json_with_id_stats("id-plain-impossible.parquet", 10, 1, 50, 0),
4446 add_json("id-plain-missing-stats.parquet"),
4447 partitioned_dv_add_json("id-dv-missing-stats.parquet", r#"{}"#),
4448 ],
4449 )?;
4450 let source = load_delta_source(DeltaSourceConfig {
4451 name: "orders".to_owned(),
4452 table_uri: table.path.to_string_lossy().to_string(),
4453 version: None,
4454 storage_options: Default::default(),
4455 })?;
4456 let predicate = datafusion_expr_to_kernel_predicate(&col("id").gt(int32_lit(100)))?;
4457 let stats_scan = build_projected_predicated_stats_delta_scan(&source, Some(predicate))?;
4458
4459 assert_eq!(
4460 kernel_scan_file_boundaries(&stats_scan, source.table_uri())?,
4461 vec![
4462 KernelScanFileBoundary {
4463 path: "id-dv-missing-stats.parquet".to_owned(),
4464 has_deletion_vector: true,
4465 },
4466 KernelScanFileBoundary {
4467 path: "id-dv-possible.parquet".to_owned(),
4468 has_deletion_vector: true,
4469 },
4470 KernelScanFileBoundary {
4471 path: "id-plain-missing-stats.parquet".to_owned(),
4472 has_deletion_vector: false,
4473 },
4474 ]
4475 );
4476
4477 Ok(())
4478 }
4479
4480 #[test]
4481 fn delta_table_format_production_paths_do_not_load_dv_payloads()
4482 -> Result<(), Box<dyn std::error::Error>> {
4483 let source_root = Path::new(env!("CARGO_MANIFEST_DIR"))
4484 .join("src")
4485 .join("table_formats");
4486 let production_source = [
4487 source_root.join("delta.rs"),
4488 source_root.join("delta").join("kernel.rs"),
4489 ]
4490 .iter()
4491 .map(fs::read_to_string)
4492 .collect::<Result<Vec<_>, _>>()?
4493 .into_iter()
4494 .map(|source| {
4495 source
4496 .split("\n#[cfg(test)]")
4497 .next()
4498 .unwrap_or(source.as_str())
4499 .to_owned()
4500 })
4501 .collect::<Vec<_>>()
4502 .join("\n");
4503
4504 assert!(!production_source.contains("get_selection_vector"));
4505 assert!(!production_source.contains("get_row_indexes"));
4506
4507 Ok(())
4508 }
4509
4510 #[test]
4511 fn kernel_predicated_scan_preserves_dv_metadata_boundary()
4512 -> Result<(), Box<dyn std::error::Error>> {
4513 let table = DeltaLogTable::new_with_protocol_metadata_and_adds(
4514 "kernel-dv-partition-boundary",
4515 DELETION_VECTOR_PROTOCOL_JSON,
4516 PARTITIONED_METADATA_JSON,
4517 &[
4518 partitioned_dv_add_json("region-us-west-dv.parquet", r#"{"region":"us-west"}"#),
4519 partitioned_add_json("region-us-west-plain.parquet", r#"{"region":"us-west"}"#),
4520 partitioned_dv_add_json("region-us-east-dv.parquet", r#"{"region":"us-east"}"#),
4521 partitioned_add_json("region-us-east-plain.parquet", r#"{"region":"us-east"}"#),
4522 ],
4523 )?;
4524 let source = load_delta_source(DeltaSourceConfig {
4525 name: "orders".to_owned(),
4526 table_uri: table.path.to_string_lossy().to_string(),
4527 version: None,
4528 storage_options: Default::default(),
4529 })?;
4530 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
4531 let predicate = datafusion_expr_to_kernel_predicate(&col("region").eq(lit("us-west")))?;
4532 let predicated_scan =
4533 build_projected_predicated_delta_scan(&source, None, Some(predicate))?;
4534
4535 assert_eq!(
4536 kernel_scan_file_boundaries(&unfiltered_scan, source.table_uri())?,
4537 vec![
4538 KernelScanFileBoundary {
4539 path: "region-us-east-dv.parquet".to_owned(),
4540 has_deletion_vector: true,
4541 },
4542 KernelScanFileBoundary {
4543 path: "region-us-east-plain.parquet".to_owned(),
4544 has_deletion_vector: false,
4545 },
4546 KernelScanFileBoundary {
4547 path: "region-us-west-dv.parquet".to_owned(),
4548 has_deletion_vector: true,
4549 },
4550 KernelScanFileBoundary {
4551 path: "region-us-west-plain.parquet".to_owned(),
4552 has_deletion_vector: false,
4553 },
4554 ]
4555 );
4556 assert_eq!(
4557 kernel_scan_file_boundaries(&predicated_scan, source.table_uri())?,
4558 vec![
4559 KernelScanFileBoundary {
4560 path: "region-us-west-dv.parquet".to_owned(),
4561 has_deletion_vector: true,
4562 },
4563 KernelScanFileBoundary {
4564 path: "region-us-west-plain.parquet".to_owned(),
4565 has_deletion_vector: false,
4566 },
4567 ]
4568 );
4569
4570 Ok(())
4571 }
4572
4573 #[test]
4574 fn kernel_partition_characterization_documents_string_null_and_empty_semantics()
4575 -> Result<(), Box<dyn std::error::Error>> {
4576 let (_table, source) = kernel_partition_characterization_source()?;
4577 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
4578
4579 assert_eq!(
4580 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
4581 vec![
4582 "region-empty-string.parquet",
4583 "region-missing.parquet",
4584 "region-null.parquet",
4585 "region-us-east.parquet",
4586 "region-us-west.parquet",
4587 ]
4588 );
4589 assert_eq!(
4590 kernel_predicated_file_paths(&source, &col("region").is_null())?,
4591 vec![
4592 "region-empty-string.parquet",
4593 "region-missing.parquet",
4594 "region-null.parquet",
4595 ]
4596 );
4597 assert_eq!(
4598 kernel_predicated_file_paths(&source, &col("region").is_not_null())?,
4599 vec!["region-us-east.parquet", "region-us-west.parquet"]
4600 );
4601 assert_eq!(
4602 kernel_predicated_file_paths(&source, &col("region").eq(lit("us-west")))?,
4603 vec!["region-us-west.parquet"]
4604 );
4605 assert_eq!(
4606 kernel_predicated_file_paths(&source, &col("region").not_eq(lit("us-west")))?,
4607 vec!["region-us-east.parquet"]
4608 );
4609 assert_eq!(
4610 kernel_predicated_file_paths(&source, &col("region").eq(lit("")))?,
4611 Vec::<String>::new()
4612 );
4613 assert_eq!(
4614 kernel_predicated_file_paths(&source, &col("region").not_eq(lit("")))?,
4615 vec!["region-us-east.parquet", "region-us-west.parquet"]
4616 );
4617 assert_eq!(
4618 kernel_predicated_file_paths(
4619 &source,
4620 &col("region").in_list(vec![lit("us-west"), lit("")], false),
4621 )?,
4622 vec!["region-us-west.parquet"]
4623 );
4624 assert_eq!(
4625 kernel_predicated_file_paths(
4626 &source,
4627 &col("region").in_list(vec![lit("us-west"), lit("")], true),
4628 )?,
4629 vec!["region-us-east.parquet"]
4630 );
4631
4632 Ok(())
4633 }
4634
4635 #[test]
4636 fn kernel_partition_characterization_documents_boolean_null_and_empty_semantics()
4637 -> Result<(), Box<dyn std::error::Error>> {
4638 let (_table, source) = kernel_boolean_partition_characterization_source()?;
4639 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
4640
4641 assert_eq!(
4642 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
4643 vec![
4644 "boolean-empty.parquet",
4645 "boolean-false.parquet",
4646 "boolean-missing.parquet",
4647 "boolean-null.parquet",
4648 "boolean-true.parquet",
4649 ]
4650 );
4651 assert_eq!(
4652 kernel_predicated_file_paths(&source, &col("is_current").is_null())?,
4653 vec![
4654 "boolean-empty.parquet",
4655 "boolean-missing.parquet",
4656 "boolean-null.parquet",
4657 ]
4658 );
4659 assert_eq!(
4660 kernel_predicated_file_paths(&source, &col("is_current").is_not_null())?,
4661 vec!["boolean-false.parquet", "boolean-true.parquet"]
4662 );
4663 assert_eq!(
4664 kernel_predicated_file_paths(&source, &col("is_current").eq(bool_lit(true)))?,
4665 vec!["boolean-true.parquet"]
4666 );
4667 assert_eq!(
4668 kernel_predicated_file_paths(&source, &col("is_current").eq(bool_lit(false)))?,
4669 vec!["boolean-false.parquet"]
4670 );
4671 assert_eq!(
4672 kernel_predicated_file_paths(&source, &col("is_current").not_eq(bool_lit(true)))?,
4673 vec!["boolean-false.parquet"]
4674 );
4675
4676 Ok(())
4677 }
4678
4679 #[test]
4680 fn kernel_partition_characterization_documents_boolean_membership_and_composition()
4681 -> Result<(), Box<dyn std::error::Error>> {
4682 let (_table, source) = kernel_boolean_partition_characterization_source()?;
4683
4684 assert_eq!(
4685 kernel_predicated_file_paths(
4686 &source,
4687 &col("is_current").in_list(vec![bool_lit(true), bool_lit(false)], false),
4688 )?,
4689 vec!["boolean-false.parquet", "boolean-true.parquet"]
4690 );
4691 assert_eq!(
4692 kernel_predicated_file_paths(
4693 &source,
4694 &col("is_current").in_list(vec![bool_lit(true)], true),
4695 )?,
4696 vec!["boolean-false.parquet"]
4697 );
4698 assert_eq!(
4699 kernel_predicated_file_paths(
4700 &source,
4701 &col("is_current")
4702 .eq(bool_lit(true))
4703 .or(col("is_current").is_null()),
4704 )?,
4705 vec![
4706 "boolean-empty.parquet",
4707 "boolean-missing.parquet",
4708 "boolean-null.parquet",
4709 "boolean-true.parquet",
4710 ]
4711 );
4712 assert_eq!(
4713 kernel_predicated_file_paths(
4714 &source,
4715 &col("is_current")
4716 .eq(bool_lit(true))
4717 .and(col("is_current").is_not_null()),
4718 )?,
4719 vec!["boolean-true.parquet"]
4720 );
4721 assert_eq!(
4722 kernel_predicated_file_paths(
4723 &source,
4724 &Expr::Not(Box::new(col("is_current").eq(bool_lit(true)))),
4725 )?,
4726 vec!["boolean-false.parquet"]
4727 );
4728
4729 Ok(())
4730 }
4731
4732 #[test]
4733 fn kernel_partition_characterization_documents_date_ordering()
4734 -> Result<(), Box<dyn std::error::Error>> {
4735 let (_table, source) = kernel_date_partition_characterization_source()?;
4736 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
4737
4738 assert_eq!(
4739 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
4740 vec![
4741 "date-empty.parquet",
4742 "date-epoch.parquet",
4743 "date-leap-day.parquet",
4744 "date-missing.parquet",
4745 "date-new-year.parquet",
4746 "date-null.parquet",
4747 "date-pre-epoch.parquet",
4748 ]
4749 );
4750 assert_eq!(
4751 kernel_predicated_file_paths(&source, &col("event_date").gt(date_lit(19_782)))?,
4752 vec!["date-new-year.parquet"]
4753 );
4754 assert_eq!(
4755 kernel_predicated_file_paths(&source, &col("event_date").lt_eq(date_lit(0)))?,
4756 vec!["date-epoch.parquet", "date-pre-epoch.parquet"]
4757 );
4758 assert_eq!(
4759 kernel_predicated_file_paths(&source, &date_lit(20_454).gt(col("event_date")))?,
4760 vec![
4761 "date-epoch.parquet",
4762 "date-leap-day.parquet",
4763 "date-pre-epoch.parquet",
4764 ]
4765 );
4766
4767 Ok(())
4768 }
4769
4770 #[test]
4771 fn kernel_partition_characterization_documents_date_null_and_empty_semantics()
4772 -> Result<(), Box<dyn std::error::Error>> {
4773 let (_table, source) = kernel_date_partition_characterization_source()?;
4774
4775 assert_eq!(
4776 kernel_predicated_file_paths(&source, &col("event_date").is_null())?,
4777 vec![
4778 "date-empty.parquet",
4779 "date-missing.parquet",
4780 "date-null.parquet"
4781 ]
4782 );
4783 assert_eq!(
4784 kernel_predicated_file_paths(&source, &col("event_date").is_not_null())?,
4785 vec![
4786 "date-epoch.parquet",
4787 "date-leap-day.parquet",
4788 "date-new-year.parquet",
4789 "date-pre-epoch.parquet",
4790 ]
4791 );
4792 assert_eq!(
4793 kernel_predicated_file_paths(&source, &col("event_date").eq(date_lit(20_454)))?,
4794 vec!["date-new-year.parquet"]
4795 );
4796 assert_eq!(
4797 kernel_predicated_file_paths(&source, &col("event_date").not_eq(date_lit(20_454)))?,
4798 vec![
4799 "date-epoch.parquet",
4800 "date-leap-day.parquet",
4801 "date-pre-epoch.parquet",
4802 ]
4803 );
4804
4805 Ok(())
4806 }
4807
4808 #[test]
4809 fn kernel_partition_characterization_documents_date_membership_between_and_composition()
4810 -> Result<(), Box<dyn std::error::Error>> {
4811 let (_table, source) = kernel_date_partition_characterization_source()?;
4812
4813 assert_eq!(
4814 kernel_predicated_file_paths(
4815 &source,
4816 &col("event_date").in_list(vec![date_lit(-1), date_lit(20_454)], false),
4817 )?,
4818 vec!["date-new-year.parquet", "date-pre-epoch.parquet"]
4819 );
4820 assert_eq!(
4821 kernel_predicated_file_paths(
4822 &source,
4823 &col("event_date").in_list(vec![date_lit(19_782)], true),
4824 )?,
4825 vec![
4826 "date-epoch.parquet",
4827 "date-new-year.parquet",
4828 "date-pre-epoch.parquet",
4829 ]
4830 );
4831 assert_eq!(
4832 kernel_predicated_file_paths(
4833 &source,
4834 &col("event_date").between(date_lit(-1), date_lit(19_782)),
4835 )?,
4836 vec![
4837 "date-epoch.parquet",
4838 "date-leap-day.parquet",
4839 "date-pre-epoch.parquet",
4840 ]
4841 );
4842 assert_eq!(
4843 kernel_predicated_file_paths(
4844 &source,
4845 &col("event_date").not_between(date_lit(-1), date_lit(19_782)),
4846 )?,
4847 vec!["date-new-year.parquet"]
4848 );
4849 assert_eq!(
4850 kernel_predicated_file_paths(
4851 &source,
4852 &col("event_date")
4853 .gt_eq(date_lit(0))
4854 .and(col("event_date").lt(date_lit(20_454))),
4855 )?,
4856 vec!["date-epoch.parquet", "date-leap-day.parquet"]
4857 );
4858 assert_eq!(
4859 kernel_predicated_file_paths(
4860 &source,
4861 &col("event_date")
4862 .eq(date_lit(-1))
4863 .or(col("event_date").eq(date_lit(20_454))),
4864 )?,
4865 vec!["date-new-year.parquet", "date-pre-epoch.parquet"]
4866 );
4867 assert_eq!(
4868 kernel_predicated_file_paths(
4869 &source,
4870 &Expr::Not(Box::new(col("event_date").eq(date_lit(19_782)))),
4871 )?,
4872 vec![
4873 "date-epoch.parquet",
4874 "date-new-year.parquet",
4875 "date-pre-epoch.parquet",
4876 ]
4877 );
4878
4879 Ok(())
4880 }
4881
4882 #[test]
4883 fn kernel_partition_characterization_documents_decimal_numeric_ordering()
4884 -> Result<(), Box<dyn std::error::Error>> {
4885 let (_table, source) = kernel_decimal_partition_characterization_source()?;
4886 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
4887
4888 assert_eq!(
4889 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
4890 vec![
4891 "decimal-empty.parquet",
4892 "decimal-large.parquet",
4893 "decimal-missing.parquet",
4894 "decimal-negative.parquet",
4895 "decimal-null.parquet",
4896 "decimal-ten.parquet",
4897 "decimal-two.parquet",
4898 "decimal-zero.parquet",
4899 ]
4900 );
4901 assert_eq!(
4902 kernel_predicated_file_paths(&source, &col("amount").gt(decimal_lit(200)))?,
4903 vec!["decimal-large.parquet", "decimal-ten.parquet"]
4904 );
4905 assert_eq!(
4906 kernel_predicated_file_paths(&source, &col("amount").lt(decimal_lit(1000)))?,
4907 vec![
4908 "decimal-negative.parquet",
4909 "decimal-two.parquet",
4910 "decimal-zero.parquet",
4911 ]
4912 );
4913 assert_eq!(
4914 kernel_predicated_file_paths(&source, &decimal_lit(1000).gt(col("amount")))?,
4915 vec![
4916 "decimal-negative.parquet",
4917 "decimal-two.parquet",
4918 "decimal-zero.parquet",
4919 ]
4920 );
4921
4922 Ok(())
4923 }
4924
4925 #[test]
4926 fn kernel_partition_characterization_documents_decimal_null_and_empty_semantics()
4927 -> Result<(), Box<dyn std::error::Error>> {
4928 let (_table, source) = kernel_decimal_partition_characterization_source()?;
4929
4930 assert_eq!(
4931 kernel_predicated_file_paths(&source, &col("amount").is_null())?,
4932 vec![
4933 "decimal-empty.parquet",
4934 "decimal-missing.parquet",
4935 "decimal-null.parquet",
4936 ]
4937 );
4938 assert_eq!(
4939 kernel_predicated_file_paths(&source, &col("amount").is_not_null())?,
4940 vec![
4941 "decimal-large.parquet",
4942 "decimal-negative.parquet",
4943 "decimal-ten.parquet",
4944 "decimal-two.parquet",
4945 "decimal-zero.parquet",
4946 ]
4947 );
4948 assert_eq!(
4949 kernel_predicated_file_paths(&source, &col("amount").eq(decimal_lit(12_345)))?,
4950 vec!["decimal-large.parquet"]
4951 );
4952 assert_eq!(
4953 kernel_predicated_file_paths(&source, &col("amount").not_eq(decimal_lit(12_345)))?,
4954 vec![
4955 "decimal-negative.parquet",
4956 "decimal-ten.parquet",
4957 "decimal-two.parquet",
4958 "decimal-zero.parquet",
4959 ]
4960 );
4961
4962 Ok(())
4963 }
4964
4965 #[test]
4966 fn kernel_partition_characterization_documents_decimal_membership_between_and_composition()
4967 -> Result<(), Box<dyn std::error::Error>> {
4968 let (_table, source) = kernel_decimal_partition_characterization_source()?;
4969
4970 assert_eq!(
4971 kernel_predicated_file_paths(
4972 &source,
4973 &col("amount").in_list(vec![decimal_lit(-123), decimal_lit(12_345)], false),
4974 )?,
4975 vec!["decimal-large.parquet", "decimal-negative.parquet"]
4976 );
4977 assert_eq!(
4978 kernel_predicated_file_paths(
4979 &source,
4980 &col("amount").in_list(vec![decimal_lit(200)], true),
4981 )?,
4982 vec![
4983 "decimal-large.parquet",
4984 "decimal-negative.parquet",
4985 "decimal-ten.parquet",
4986 "decimal-zero.parquet",
4987 ]
4988 );
4989 assert_eq!(
4990 kernel_predicated_file_paths(
4991 &source,
4992 &col("amount").between(decimal_lit(-123), decimal_lit(200)),
4993 )?,
4994 vec![
4995 "decimal-negative.parquet",
4996 "decimal-two.parquet",
4997 "decimal-zero.parquet",
4998 ]
4999 );
5000 assert_eq!(
5001 kernel_predicated_file_paths(
5002 &source,
5003 &col("amount").not_between(decimal_lit(-123), decimal_lit(200)),
5004 )?,
5005 vec!["decimal-large.parquet", "decimal-ten.parquet"]
5006 );
5007 assert_eq!(
5008 kernel_predicated_file_paths(
5009 &source,
5010 &col("amount")
5011 .gt_eq(decimal_lit(0))
5012 .and(col("amount").lt(decimal_lit(12345))),
5013 )?,
5014 vec![
5015 "decimal-ten.parquet",
5016 "decimal-two.parquet",
5017 "decimal-zero.parquet",
5018 ]
5019 );
5020 assert_eq!(
5021 kernel_predicated_file_paths(
5022 &source,
5023 &col("amount")
5024 .eq(decimal_lit(-123))
5025 .or(col("amount").eq(decimal_lit(12_345))),
5026 )?,
5027 vec!["decimal-large.parquet", "decimal-negative.parquet"]
5028 );
5029 assert_eq!(
5030 kernel_predicated_file_paths(
5031 &source,
5032 &Expr::Not(Box::new(col("amount").eq(decimal_lit(200)))),
5033 )?,
5034 vec![
5035 "decimal-large.parquet",
5036 "decimal-negative.parquet",
5037 "decimal-ten.parquet",
5038 "decimal-zero.parquet",
5039 ]
5040 );
5041
5042 Ok(())
5043 }
5044
5045 #[test]
5046 fn kernel_partition_characterization_documents_invalid_decimal_metadata()
5047 -> Result<(), Box<dyn std::error::Error>> {
5048 let invalid_text_table = DeltaLogTable::new_with_metadata_and_adds(
5049 "kernel-invalid-decimal-characterization",
5050 DECIMAL_PARTITIONED_METADATA_JSON,
5051 &[
5052 partitioned_add_json("decimal-valid.parquet", r#"{"amount":"123.45"}"#),
5053 partitioned_add_json("decimal-invalid.parquet", r#"{"amount":"not-a-decimal"}"#),
5054 ],
5055 )?;
5056 let invalid_text_source = load_delta_source(DeltaSourceConfig {
5057 name: "orders".to_owned(),
5058 table_uri: invalid_text_table.path.to_string_lossy().to_string(),
5059 version: None,
5060 storage_options: Default::default(),
5061 })?;
5062 let unfiltered_scan = build_projected_delta_scan(&invalid_text_source, None)?;
5063
5064 assert_invalid_decimal_partition_error(kernel_scan_file_paths(
5065 &unfiltered_scan,
5066 invalid_text_source.table_uri(),
5067 ))?;
5068 assert_invalid_decimal_partition_error(kernel_predicated_file_paths(
5069 &invalid_text_source,
5070 &col("amount").eq(decimal_lit(12_345)),
5071 ))?;
5072
5073 let scale_mismatch_table = DeltaLogTable::new_with_metadata_and_adds(
5074 "kernel-invalid-decimal-scale-characterization",
5075 DECIMAL_PARTITIONED_METADATA_JSON,
5076 &[
5077 partitioned_add_json("decimal-valid.parquet", r#"{"amount":"123.45"}"#),
5078 partitioned_add_json("decimal-scale.parquet", r#"{"amount":"123.450"}"#),
5079 ],
5080 )?;
5081 let scale_mismatch_source = load_delta_source(DeltaSourceConfig {
5082 name: "orders".to_owned(),
5083 table_uri: scale_mismatch_table.path.to_string_lossy().to_string(),
5084 version: None,
5085 storage_options: Default::default(),
5086 })?;
5087
5088 assert_invalid_decimal_partition_error(kernel_predicated_file_paths(
5089 &scale_mismatch_source,
5090 &col("amount").eq(decimal_lit(12_345)),
5091 ))?;
5092
5093 Ok(())
5094 }
5095
5096 #[test]
5097 fn kernel_partition_characterization_documents_floating_numeric_ordering()
5098 -> Result<(), Box<dyn std::error::Error>> {
5099 let (_table, source) = kernel_floating_partition_characterization_source()?;
5100 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
5101
5102 assert_eq!(
5103 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
5104 vec![
5105 "floating-empty.parquet",
5106 "floating-missing.parquet",
5107 "floating-neg-zero.parquet",
5108 "floating-neg.parquet",
5109 "floating-null.parquet",
5110 "floating-one.parquet",
5111 "floating-pos-zero.parquet",
5112 "floating-ten.parquet",
5113 ]
5114 );
5115 assert_eq!(
5116 kernel_predicated_file_paths(&source, &col("float_part").gt(float32_lit(1.5)))?,
5117 vec!["floating-ten.parquet"]
5118 );
5119 assert_eq!(
5120 kernel_predicated_file_paths(&source, &col("double_part").lt(float64_lit(0.0)))?,
5121 vec!["floating-neg-zero.parquet", "floating-neg.parquet"]
5122 );
5123 assert_eq!(
5124 kernel_predicated_file_paths(&source, &float64_lit(0.0).gt(col("double_part")))?,
5125 vec!["floating-neg-zero.parquet", "floating-neg.parquet"]
5126 );
5127 assert_eq!(
5128 kernel_predicated_file_paths(&source, &col("float_part").lt(float32_lit(10.0)))?,
5129 vec![
5130 "floating-neg-zero.parquet",
5131 "floating-neg.parquet",
5132 "floating-one.parquet",
5133 "floating-pos-zero.parquet",
5134 ]
5135 );
5136
5137 Ok(())
5138 }
5139
5140 #[test]
5141 fn kernel_partition_characterization_documents_floating_null_and_zero_semantics()
5142 -> Result<(), Box<dyn std::error::Error>> {
5143 let (_table, source) = kernel_floating_partition_characterization_source()?;
5144
5145 assert_eq!(
5146 kernel_predicated_file_paths(&source, &col("float_part").is_null())?,
5147 vec![
5148 "floating-empty.parquet",
5149 "floating-missing.parquet",
5150 "floating-null.parquet",
5151 ]
5152 );
5153 assert_eq!(
5154 kernel_predicated_file_paths(&source, &col("double_part").is_not_null())?,
5155 vec![
5156 "floating-neg-zero.parquet",
5157 "floating-neg.parquet",
5158 "floating-one.parquet",
5159 "floating-pos-zero.parquet",
5160 "floating-ten.parquet",
5161 ]
5162 );
5163 assert_eq!(
5164 kernel_predicated_file_paths(&source, &col("float_part").eq(float32_lit(-0.0)))?,
5165 vec!["floating-neg-zero.parquet"]
5166 );
5167 assert_eq!(
5168 kernel_predicated_file_paths(&source, &col("float_part").eq(float32_lit(0.0)))?,
5169 vec!["floating-pos-zero.parquet"]
5170 );
5171 assert_eq!(
5172 kernel_predicated_file_paths(&source, &col("double_part").not_eq(float64_lit(0.0)))?,
5173 vec![
5174 "floating-neg-zero.parquet",
5175 "floating-neg.parquet",
5176 "floating-one.parquet",
5177 "floating-ten.parquet",
5178 ]
5179 );
5180
5181 Ok(())
5182 }
5183
5184 #[test]
5185 fn kernel_partition_characterization_documents_floating_membership_between_and_composition()
5186 -> Result<(), Box<dyn std::error::Error>> {
5187 let (_table, source) = kernel_floating_partition_characterization_source()?;
5188
5189 assert_eq!(
5190 kernel_predicated_file_paths(
5191 &source,
5192 &col("float_part").in_list(vec![float32_lit(-1.5), float32_lit(1.5)], false),
5193 )?,
5194 vec!["floating-neg.parquet", "floating-one.parquet"]
5195 );
5196 assert_eq!(
5197 kernel_predicated_file_paths(
5198 &source,
5199 &col("double_part").in_list(vec![float64_lit(2.25)], true),
5200 )?,
5201 vec![
5202 "floating-neg-zero.parquet",
5203 "floating-neg.parquet",
5204 "floating-pos-zero.parquet",
5205 "floating-ten.parquet",
5206 ]
5207 );
5208 assert_eq!(
5209 kernel_predicated_file_paths(
5210 &source,
5211 &col("float_part").between(float32_lit(-0.0), float32_lit(1.5)),
5212 )?,
5213 vec![
5214 "floating-neg-zero.parquet",
5215 "floating-one.parquet",
5216 "floating-pos-zero.parquet",
5217 ]
5218 );
5219 assert_eq!(
5220 kernel_predicated_file_paths(
5221 &source,
5222 &col("double_part").not_between(float64_lit(0.0), float64_lit(2.25)),
5223 )?,
5224 vec![
5225 "floating-neg-zero.parquet",
5226 "floating-neg.parquet",
5227 "floating-ten.parquet",
5228 ]
5229 );
5230 assert_eq!(
5231 kernel_predicated_file_paths(
5232 &source,
5233 &col("float_part")
5234 .gt_eq(float32_lit(0.0))
5235 .and(col("float_part").lt(float32_lit(10.0))),
5236 )?,
5237 vec!["floating-one.parquet", "floating-pos-zero.parquet"]
5238 );
5239 assert_eq!(
5240 kernel_predicated_file_paths(
5241 &source,
5242 &col("double_part")
5243 .eq(float64_lit(-2.25))
5244 .or(col("double_part").eq(float64_lit(10.0))),
5245 )?,
5246 vec!["floating-neg.parquet", "floating-ten.parquet"]
5247 );
5248 assert_eq!(
5249 kernel_predicated_file_paths(
5250 &source,
5251 &Expr::Not(Box::new(col("float_part").eq(float32_lit(1.5)))),
5252 )?,
5253 vec![
5254 "floating-neg-zero.parquet",
5255 "floating-neg.parquet",
5256 "floating-pos-zero.parquet",
5257 "floating-ten.parquet",
5258 ]
5259 );
5260
5261 Ok(())
5262 }
5263
5264 #[test]
5265 fn kernel_partition_characterization_documents_binary_null_empty_and_equality_semantics()
5266 -> Result<(), Box<dyn std::error::Error>> {
5267 let (_table, source) = kernel_binary_partition_characterization_source()?;
5268 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
5269
5270 assert_eq!(
5271 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
5272 vec![
5273 "binary-HELLO.parquet",
5274 "binary-empty.parquet",
5275 "binary-hello.parquet",
5276 "binary-missing.parquet",
5277 "binary-null.parquet",
5278 "binary-special.parquet",
5279 ]
5280 );
5281 assert_eq!(
5282 kernel_predicate_file_paths(
5283 &source,
5284 DeltaKernelPredicate::new(Predicate::is_null(binary_partition_column())),
5285 )?,
5286 vec![
5287 "binary-empty.parquet",
5288 "binary-missing.parquet",
5289 "binary-null.parquet",
5290 ]
5291 );
5292 assert_eq!(
5293 kernel_predicate_file_paths(
5294 &source,
5295 DeltaKernelPredicate::new(Predicate::is_not_null(binary_partition_column())),
5296 )?,
5297 vec![
5298 "binary-HELLO.parquet",
5299 "binary-hello.parquet",
5300 "binary-special.parquet",
5301 ]
5302 );
5303 assert_eq!(
5304 kernel_predicate_file_paths(&source, binary_partition_eq(b"HELLO"))?,
5305 vec!["binary-HELLO.parquet"]
5306 );
5307 assert_eq!(
5308 kernel_predicate_file_paths(&source, binary_partition_eq(b"hello"))?,
5309 vec!["binary-hello.parquet"]
5310 );
5311 assert_eq!(
5312 kernel_predicate_file_paths(&source, binary_partition_ne(b"hello"))?,
5313 vec!["binary-HELLO.parquet", "binary-special.parquet"]
5314 );
5315 assert_eq!(
5316 kernel_predicate_file_paths(&source, binary_partition_eq(&[]))?,
5317 Vec::<String>::new()
5318 );
5319 assert_eq!(
5320 kernel_predicate_file_paths(&source, binary_partition_ne(&[]))?,
5321 vec![
5322 "binary-HELLO.parquet",
5323 "binary-hello.parquet",
5324 "binary-special.parquet",
5325 ]
5326 );
5327
5328 Ok(())
5329 }
5330
5331 #[test]
5332 fn kernel_partition_characterization_documents_binary_membership_and_composition()
5333 -> Result<(), Box<dyn std::error::Error>> {
5334 let (_table, source) = kernel_binary_partition_characterization_source()?;
5335
5336 assert_eq!(
5337 kernel_predicate_file_paths(
5338 &source,
5339 DeltaKernelPredicate::new(Predicate::or(
5340 Predicate::eq(binary_partition_column(), binary_partition_value(b"HELLO")),
5341 Predicate::eq(binary_partition_column(), binary_partition_value(b"/=%")),
5342 )),
5343 )?,
5344 vec!["binary-HELLO.parquet", "binary-special.parquet"]
5345 );
5346 assert_eq!(
5347 kernel_predicate_file_paths(
5348 &source,
5349 DeltaKernelPredicate::new(Predicate::and(
5350 Predicate::ne(binary_partition_column(), binary_partition_value(b"hello")),
5351 Predicate::ne(binary_partition_column(), binary_partition_value(b"/=%")),
5352 )),
5353 )?,
5354 vec!["binary-HELLO.parquet"]
5355 );
5356 assert_eq!(
5357 kernel_predicate_file_paths(
5358 &source,
5359 DeltaKernelPredicate::new(Predicate::and(
5360 Predicate::is_not_null(binary_partition_column()),
5361 Predicate::ne(binary_partition_column(), binary_partition_value(b"HELLO")),
5362 )),
5363 )?,
5364 vec!["binary-hello.parquet", "binary-special.parquet"]
5365 );
5366 assert_eq!(
5367 kernel_predicate_file_paths(
5368 &source,
5369 DeltaKernelPredicate::new(Predicate::or(
5370 Predicate::eq(binary_partition_column(), binary_partition_value(b"HELLO")),
5371 Predicate::is_null(binary_partition_column()),
5372 )),
5373 )?,
5374 vec![
5375 "binary-HELLO.parquet",
5376 "binary-empty.parquet",
5377 "binary-missing.parquet",
5378 "binary-null.parquet",
5379 ]
5380 );
5381 assert_eq!(
5382 kernel_predicate_file_paths(
5383 &source,
5384 DeltaKernelPredicate::new(Predicate::not(Predicate::eq(
5385 binary_partition_column(),
5386 binary_partition_value(b"hello"),
5387 ))),
5388 )?,
5389 vec!["binary-HELLO.parquet", "binary-special.parquet"]
5390 );
5391
5392 Ok(())
5393 }
5394
5395 #[test]
5396 fn kernel_partition_characterization_documents_non_utf8_binary_literals_do_not_match()
5397 -> Result<(), Box<dyn std::error::Error>> {
5398 let (_table, source) = kernel_binary_partition_characterization_source()?;
5399
5400 assert_eq!(
5401 kernel_predicate_file_paths(&source, binary_partition_eq(&[0xDE, 0xAD, 0xBE, 0xEF]))?,
5402 Vec::<String>::new()
5403 );
5404 assert_eq!(
5405 kernel_predicate_file_paths(&source, binary_partition_ne(&[0xDE, 0xAD, 0xBE, 0xEF]))?,
5406 vec![
5407 "binary-HELLO.parquet",
5408 "binary-hello.parquet",
5409 "binary-special.parquet",
5410 ]
5411 );
5412
5413 Ok(())
5414 }
5415
5416 #[test]
5417 fn kernel_partition_characterization_documents_timestamp_ordering_and_precision()
5418 -> Result<(), Box<dyn std::error::Error>> {
5419 let (_table, source) = kernel_timestamp_partition_characterization_source()?;
5420 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
5421 let pre_epoch = -1_i64;
5422 let low = 1_767_225_599_999_999_i64;
5423 let target = 1_767_225_600_123_456_i64;
5424 let high = 1_767_225_600_123_457_i64;
5425
5426 assert_eq!(
5427 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
5428 vec![
5429 "timestamp-empty.parquet",
5430 "timestamp-high.parquet",
5431 "timestamp-low.parquet",
5432 "timestamp-missing.parquet",
5433 "timestamp-null.parquet",
5434 "timestamp-pre-epoch.parquet",
5435 "timestamp-target.parquet",
5436 ]
5437 );
5438 assert_eq!(
5439 kernel_predicate_file_paths(
5440 &source,
5441 DeltaKernelPredicate::new(Predicate::lt(
5442 timestamp_partition_column(),
5443 timestamp_partition_value(target),
5444 )),
5445 )?,
5446 vec!["timestamp-low.parquet", "timestamp-pre-epoch.parquet"]
5447 );
5448 assert_eq!(
5449 kernel_predicate_file_paths(
5450 &source,
5451 DeltaKernelPredicate::new(Predicate::ge(
5452 timestamp_partition_column(),
5453 timestamp_partition_value(target),
5454 )),
5455 )?,
5456 vec!["timestamp-high.parquet", "timestamp-target.parquet"]
5457 );
5458 assert_eq!(
5459 kernel_predicate_file_paths(
5460 &source,
5461 DeltaKernelPredicate::new(Predicate::gt(
5462 timestamp_partition_value(high),
5463 timestamp_partition_column(),
5464 )),
5465 )?,
5466 vec![
5467 "timestamp-low.parquet",
5468 "timestamp-pre-epoch.parquet",
5469 "timestamp-target.parquet",
5470 ]
5471 );
5472 assert_eq!(
5473 kernel_predicate_file_paths(&source, timestamp_partition_eq(pre_epoch))?,
5474 vec!["timestamp-pre-epoch.parquet"]
5475 );
5476 assert_eq!(
5477 kernel_predicate_file_paths(&source, timestamp_partition_eq(target))?,
5478 vec!["timestamp-target.parquet"]
5479 );
5480 assert_eq!(
5481 kernel_predicate_file_paths(&source, timestamp_partition_ne(target))?,
5482 vec![
5483 "timestamp-high.parquet",
5484 "timestamp-low.parquet",
5485 "timestamp-pre-epoch.parquet",
5486 ]
5487 );
5488 assert_eq!(
5489 kernel_predicate_file_paths(&source, timestamp_partition_eq(target + 1))?,
5490 vec!["timestamp-high.parquet"]
5491 );
5492 assert_eq!(
5493 kernel_predicate_file_paths(&source, timestamp_partition_eq(low))?,
5494 vec!["timestamp-low.parquet"]
5495 );
5496
5497 Ok(())
5498 }
5499
5500 #[test]
5501 fn kernel_partition_characterization_documents_timestamp_null_empty_and_membership()
5502 -> Result<(), Box<dyn std::error::Error>> {
5503 let (_table, source) = kernel_timestamp_partition_characterization_source()?;
5504 let low = 1_767_225_599_999_999_i64;
5505 let target = 1_767_225_600_123_456_i64;
5506
5507 assert_eq!(
5508 kernel_predicate_file_paths(
5509 &source,
5510 DeltaKernelPredicate::new(Predicate::is_null(timestamp_partition_column())),
5511 )?,
5512 vec![
5513 "timestamp-empty.parquet",
5514 "timestamp-missing.parquet",
5515 "timestamp-null.parquet",
5516 ]
5517 );
5518 assert_eq!(
5519 kernel_predicate_file_paths(
5520 &source,
5521 DeltaKernelPredicate::new(Predicate::is_not_null(timestamp_partition_column())),
5522 )?,
5523 vec![
5524 "timestamp-high.parquet",
5525 "timestamp-low.parquet",
5526 "timestamp-pre-epoch.parquet",
5527 "timestamp-target.parquet",
5528 ]
5529 );
5530 assert_eq!(
5531 kernel_predicate_file_paths(
5532 &source,
5533 DeltaKernelPredicate::new(Predicate::or(
5534 Predicate::eq(timestamp_partition_column(), timestamp_partition_value(low)),
5535 Predicate::eq(
5536 timestamp_partition_column(),
5537 timestamp_partition_value(target)
5538 ),
5539 )),
5540 )?,
5541 vec!["timestamp-low.parquet", "timestamp-target.parquet"]
5542 );
5543 assert_eq!(
5544 kernel_predicate_file_paths(
5545 &source,
5546 DeltaKernelPredicate::new(Predicate::and(
5547 Predicate::ne(timestamp_partition_column(), timestamp_partition_value(low)),
5548 Predicate::ne(
5549 timestamp_partition_column(),
5550 timestamp_partition_value(target)
5551 ),
5552 )),
5553 )?,
5554 vec!["timestamp-high.parquet", "timestamp-pre-epoch.parquet"]
5555 );
5556
5557 Ok(())
5558 }
5559
5560 #[test]
5561 fn kernel_partition_characterization_documents_timestamp_between_and_composition()
5562 -> Result<(), Box<dyn std::error::Error>> {
5563 let (_table, source) = kernel_timestamp_partition_characterization_source()?;
5564 let low = 1_767_225_599_999_999_i64;
5565 let target = 1_767_225_600_123_456_i64;
5566 let high = 1_767_225_600_123_457_i64;
5567
5568 assert_eq!(
5569 kernel_predicate_file_paths(
5570 &source,
5571 DeltaKernelPredicate::new(Predicate::and(
5572 Predicate::ge(timestamp_partition_column(), timestamp_partition_value(low)),
5573 Predicate::le(
5574 timestamp_partition_column(),
5575 timestamp_partition_value(target)
5576 ),
5577 )),
5578 )?,
5579 vec!["timestamp-low.parquet", "timestamp-target.parquet"]
5580 );
5581 assert_eq!(
5582 kernel_predicate_file_paths(
5583 &source,
5584 DeltaKernelPredicate::new(Predicate::or(
5585 Predicate::lt(timestamp_partition_column(), timestamp_partition_value(low)),
5586 Predicate::gt(
5587 timestamp_partition_column(),
5588 timestamp_partition_value(target)
5589 ),
5590 )),
5591 )?,
5592 vec!["timestamp-high.parquet", "timestamp-pre-epoch.parquet"]
5593 );
5594 assert_eq!(
5595 kernel_predicate_file_paths(
5596 &source,
5597 DeltaKernelPredicate::new(Predicate::and(
5598 Predicate::gt(timestamp_partition_column(), timestamp_partition_value(low)),
5599 Predicate::le(
5600 timestamp_partition_column(),
5601 timestamp_partition_value(high)
5602 ),
5603 )),
5604 )?,
5605 vec!["timestamp-high.parquet", "timestamp-target.parquet"]
5606 );
5607 assert_eq!(
5608 kernel_predicate_file_paths(
5609 &source,
5610 DeltaKernelPredicate::new(Predicate::or(
5611 Predicate::eq(timestamp_partition_column(), timestamp_partition_value(low)),
5612 Predicate::is_null(timestamp_partition_column()),
5613 )),
5614 )?,
5615 vec![
5616 "timestamp-empty.parquet",
5617 "timestamp-low.parquet",
5618 "timestamp-missing.parquet",
5619 "timestamp-null.parquet",
5620 ]
5621 );
5622 assert_eq!(
5623 kernel_predicate_file_paths(
5624 &source,
5625 DeltaKernelPredicate::new(Predicate::not(Predicate::eq(
5626 timestamp_partition_column(),
5627 timestamp_partition_value(target),
5628 ))),
5629 )?,
5630 vec![
5631 "timestamp-high.parquet",
5632 "timestamp-low.parquet",
5633 "timestamp-pre-epoch.parquet",
5634 ]
5635 );
5636
5637 Ok(())
5638 }
5639
5640 #[test]
5641 fn kernel_partition_characterization_documents_timestamp_ntz_ordering_and_formatting()
5642 -> Result<(), Box<dyn std::error::Error>> {
5643 let (_table, source) = kernel_timestamp_ntz_partition_characterization_source()?;
5644 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
5645 let pre_epoch = -1_i64;
5646 let low = 1_767_225_599_999_999_i64;
5647 let target = 1_767_225_600_123_456_i64;
5648 let high = 1_767_225_600_123_457_i64;
5649
5650 assert_eq!(
5651 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
5652 vec![
5653 "timestamp-ntz-empty.parquet",
5654 "timestamp-ntz-high.parquet",
5655 "timestamp-ntz-low-space.parquet",
5656 "timestamp-ntz-missing.parquet",
5657 "timestamp-ntz-null.parquet",
5658 "timestamp-ntz-pre-epoch.parquet",
5659 "timestamp-ntz-target-space.parquet",
5660 ]
5661 );
5662 assert_eq!(
5663 kernel_predicate_file_paths(
5664 &source,
5665 DeltaKernelPredicate::new(Predicate::lt(
5666 timestamp_ntz_partition_column(),
5667 timestamp_ntz_partition_value(target),
5668 )),
5669 )?,
5670 vec![
5671 "timestamp-ntz-low-space.parquet",
5672 "timestamp-ntz-pre-epoch.parquet",
5673 ]
5674 );
5675 assert_eq!(
5676 kernel_predicate_file_paths(
5677 &source,
5678 DeltaKernelPredicate::new(Predicate::ge(
5679 timestamp_ntz_partition_column(),
5680 timestamp_ntz_partition_value(target),
5681 )),
5682 )?,
5683 vec![
5684 "timestamp-ntz-high.parquet",
5685 "timestamp-ntz-target-space.parquet",
5686 ]
5687 );
5688 assert_eq!(
5689 kernel_predicate_file_paths(
5690 &source,
5691 DeltaKernelPredicate::new(Predicate::gt(
5692 timestamp_ntz_partition_value(high),
5693 timestamp_ntz_partition_column(),
5694 )),
5695 )?,
5696 vec![
5697 "timestamp-ntz-low-space.parquet",
5698 "timestamp-ntz-pre-epoch.parquet",
5699 "timestamp-ntz-target-space.parquet",
5700 ]
5701 );
5702 assert_eq!(
5703 kernel_predicate_file_paths(&source, timestamp_ntz_partition_eq(pre_epoch))?,
5704 vec!["timestamp-ntz-pre-epoch.parquet"]
5705 );
5706 assert_eq!(
5707 kernel_predicate_file_paths(&source, timestamp_ntz_partition_eq(target))?,
5708 vec!["timestamp-ntz-target-space.parquet"]
5709 );
5710 assert_eq!(
5711 kernel_predicate_file_paths(&source, timestamp_ntz_partition_ne(target))?,
5712 vec![
5713 "timestamp-ntz-high.parquet",
5714 "timestamp-ntz-low-space.parquet",
5715 "timestamp-ntz-pre-epoch.parquet",
5716 ]
5717 );
5718 assert_eq!(
5719 kernel_predicate_file_paths(&source, timestamp_ntz_partition_eq(high))?,
5720 vec!["timestamp-ntz-high.parquet"]
5721 );
5722 assert_eq!(
5723 kernel_predicate_file_paths(&source, timestamp_ntz_partition_eq(low))?,
5724 vec!["timestamp-ntz-low-space.parquet"]
5725 );
5726
5727 Ok(())
5728 }
5729
5730 #[test]
5731 fn kernel_partition_characterization_documents_timestamp_ntz_null_empty_and_membership()
5732 -> Result<(), Box<dyn std::error::Error>> {
5733 let (_table, source) = kernel_timestamp_ntz_partition_characterization_source()?;
5734 let low = 1_767_225_599_999_999_i64;
5735 let target = 1_767_225_600_123_456_i64;
5736
5737 assert_eq!(
5738 kernel_predicate_file_paths(
5739 &source,
5740 DeltaKernelPredicate::new(Predicate::is_null(timestamp_ntz_partition_column())),
5741 )?,
5742 vec![
5743 "timestamp-ntz-empty.parquet",
5744 "timestamp-ntz-missing.parquet",
5745 "timestamp-ntz-null.parquet",
5746 ]
5747 );
5748 assert_eq!(
5749 kernel_predicate_file_paths(
5750 &source,
5751 DeltaKernelPredicate::new(Predicate::is_not_null(timestamp_ntz_partition_column())),
5752 )?,
5753 vec![
5754 "timestamp-ntz-high.parquet",
5755 "timestamp-ntz-low-space.parquet",
5756 "timestamp-ntz-pre-epoch.parquet",
5757 "timestamp-ntz-target-space.parquet",
5758 ]
5759 );
5760 assert_eq!(
5761 kernel_predicate_file_paths(
5762 &source,
5763 DeltaKernelPredicate::new(Predicate::or(
5764 Predicate::eq(
5765 timestamp_ntz_partition_column(),
5766 timestamp_ntz_partition_value(low)
5767 ),
5768 Predicate::eq(
5769 timestamp_ntz_partition_column(),
5770 timestamp_ntz_partition_value(target)
5771 ),
5772 )),
5773 )?,
5774 vec![
5775 "timestamp-ntz-low-space.parquet",
5776 "timestamp-ntz-target-space.parquet",
5777 ]
5778 );
5779 assert_eq!(
5780 kernel_predicate_file_paths(
5781 &source,
5782 DeltaKernelPredicate::new(Predicate::and(
5783 Predicate::ne(
5784 timestamp_ntz_partition_column(),
5785 timestamp_ntz_partition_value(low)
5786 ),
5787 Predicate::ne(
5788 timestamp_ntz_partition_column(),
5789 timestamp_ntz_partition_value(target)
5790 ),
5791 )),
5792 )?,
5793 vec![
5794 "timestamp-ntz-high.parquet",
5795 "timestamp-ntz-pre-epoch.parquet",
5796 ]
5797 );
5798
5799 Ok(())
5800 }
5801
5802 #[test]
5803 fn kernel_partition_characterization_documents_timestamp_ntz_between_and_composition()
5804 -> Result<(), Box<dyn std::error::Error>> {
5805 let (_table, source) = kernel_timestamp_ntz_partition_characterization_source()?;
5806 let low = 1_767_225_599_999_999_i64;
5807 let target = 1_767_225_600_123_456_i64;
5808 let high = 1_767_225_600_123_457_i64;
5809
5810 assert_eq!(
5811 kernel_predicate_file_paths(
5812 &source,
5813 DeltaKernelPredicate::new(Predicate::and(
5814 Predicate::ge(
5815 timestamp_ntz_partition_column(),
5816 timestamp_ntz_partition_value(low)
5817 ),
5818 Predicate::le(
5819 timestamp_ntz_partition_column(),
5820 timestamp_ntz_partition_value(target)
5821 ),
5822 )),
5823 )?,
5824 vec![
5825 "timestamp-ntz-low-space.parquet",
5826 "timestamp-ntz-target-space.parquet",
5827 ]
5828 );
5829 assert_eq!(
5830 kernel_predicate_file_paths(
5831 &source,
5832 DeltaKernelPredicate::new(Predicate::or(
5833 Predicate::lt(
5834 timestamp_ntz_partition_column(),
5835 timestamp_ntz_partition_value(low)
5836 ),
5837 Predicate::gt(
5838 timestamp_ntz_partition_column(),
5839 timestamp_ntz_partition_value(target)
5840 ),
5841 )),
5842 )?,
5843 vec![
5844 "timestamp-ntz-high.parquet",
5845 "timestamp-ntz-pre-epoch.parquet",
5846 ]
5847 );
5848 assert_eq!(
5849 kernel_predicate_file_paths(
5850 &source,
5851 DeltaKernelPredicate::new(Predicate::and(
5852 Predicate::gt(
5853 timestamp_ntz_partition_column(),
5854 timestamp_ntz_partition_value(low)
5855 ),
5856 Predicate::le(
5857 timestamp_ntz_partition_column(),
5858 timestamp_ntz_partition_value(high)
5859 ),
5860 )),
5861 )?,
5862 vec![
5863 "timestamp-ntz-high.parquet",
5864 "timestamp-ntz-target-space.parquet",
5865 ]
5866 );
5867 assert_eq!(
5868 kernel_predicate_file_paths(
5869 &source,
5870 DeltaKernelPredicate::new(Predicate::or(
5871 Predicate::eq(
5872 timestamp_ntz_partition_column(),
5873 timestamp_ntz_partition_value(low)
5874 ),
5875 Predicate::is_null(timestamp_ntz_partition_column()),
5876 )),
5877 )?,
5878 vec![
5879 "timestamp-ntz-empty.parquet",
5880 "timestamp-ntz-low-space.parquet",
5881 "timestamp-ntz-missing.parquet",
5882 "timestamp-ntz-null.parquet",
5883 ]
5884 );
5885 assert_eq!(
5886 kernel_predicate_file_paths(
5887 &source,
5888 DeltaKernelPredicate::new(Predicate::not(Predicate::eq(
5889 timestamp_ntz_partition_column(),
5890 timestamp_ntz_partition_value(target),
5891 ))),
5892 )?,
5893 vec![
5894 "timestamp-ntz-high.parquet",
5895 "timestamp-ntz-low-space.parquet",
5896 "timestamp-ntz-pre-epoch.parquet",
5897 ]
5898 );
5899
5900 Ok(())
5901 }
5902
5903 #[test]
5904 fn kernel_partition_characterization_documents_invalid_timestamp_metadata()
5905 -> Result<(), Box<dyn std::error::Error>> {
5906 let table = DeltaLogTable::new_with_metadata_and_adds(
5907 "kernel-invalid-timestamp-characterization",
5908 TIMESTAMP_PARTITIONED_METADATA_JSON,
5909 &[
5910 partitioned_add_json(
5911 "timestamp-valid.parquet",
5912 r#"{"event_ts":"2026-01-01T00:00:00.123456Z"}"#,
5913 ),
5914 partitioned_add_json(
5915 "timestamp-invalid.parquet",
5916 r#"{"event_ts":"not-a-timestamp"}"#,
5917 ),
5918 ],
5919 )?;
5920 let source = load_delta_source(DeltaSourceConfig {
5921 name: "orders".to_owned(),
5922 table_uri: table.path.to_string_lossy().to_string(),
5923 version: None,
5924 storage_options: Default::default(),
5925 })?;
5926 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
5927
5928 assert_invalid_timestamp_partition_error(kernel_scan_file_paths(
5929 &unfiltered_scan,
5930 source.table_uri(),
5931 ))?;
5932 assert_invalid_timestamp_partition_error(kernel_predicate_file_paths(
5933 &source,
5934 timestamp_partition_eq(1_767_225_600_123_456),
5935 ))?;
5936
5937 Ok(())
5938 }
5939
5940 #[test]
5941 fn kernel_partition_characterization_documents_invalid_timestamp_ntz_metadata()
5942 -> Result<(), Box<dyn std::error::Error>> {
5943 let invalid_text_table = DeltaLogTable::new_with_protocol_metadata_and_adds(
5944 "kernel-invalid-timestamp-ntz-characterization",
5945 TIMESTAMP_NTZ_PROTOCOL_JSON,
5946 TIMESTAMP_NTZ_PARTITIONED_METADATA_JSON,
5947 &[
5948 partitioned_add_json(
5949 "timestamp-ntz-valid.parquet",
5950 r#"{"event_ts_ntz":"2026-01-01 00:00:00.123456"}"#,
5951 ),
5952 partitioned_add_json(
5953 "timestamp-ntz-invalid.parquet",
5954 r#"{"event_ts_ntz":"not-a-timestamp"}"#,
5955 ),
5956 ],
5957 )?;
5958 let invalid_text_source = load_delta_source(DeltaSourceConfig {
5959 name: "orders".to_owned(),
5960 table_uri: invalid_text_table.path.to_string_lossy().to_string(),
5961 version: None,
5962 storage_options: Default::default(),
5963 })?;
5964 let unfiltered_scan = build_projected_delta_scan(&invalid_text_source, None)?;
5965
5966 assert_invalid_timestamp_ntz_partition_error(kernel_scan_file_paths(
5967 &unfiltered_scan,
5968 invalid_text_source.table_uri(),
5969 ))?;
5970 assert_invalid_timestamp_ntz_partition_error(kernel_predicate_file_paths(
5971 &invalid_text_source,
5972 timestamp_ntz_partition_eq(1_767_225_600_123_456),
5973 ))?;
5974
5975 let t_separator_table = DeltaLogTable::new_with_protocol_metadata_and_adds(
5976 "kernel-t-separator-timestamp-ntz-characterization",
5977 TIMESTAMP_NTZ_PROTOCOL_JSON,
5978 TIMESTAMP_NTZ_PARTITIONED_METADATA_JSON,
5979 &[
5980 partitioned_add_json(
5981 "timestamp-ntz-valid.parquet",
5982 r#"{"event_ts_ntz":"2026-01-01 00:00:00.123456"}"#,
5983 ),
5984 partitioned_add_json(
5985 "timestamp-ntz-t-separator.parquet",
5986 r#"{"event_ts_ntz":"2026-01-01T00:00:00.123456"}"#,
5987 ),
5988 ],
5989 )?;
5990 let t_separator_source = load_delta_source(DeltaSourceConfig {
5991 name: "orders".to_owned(),
5992 table_uri: t_separator_table.path.to_string_lossy().to_string(),
5993 version: None,
5994 storage_options: Default::default(),
5995 })?;
5996 let unfiltered_scan = build_projected_delta_scan(&t_separator_source, None)?;
5997
5998 assert_invalid_timestamp_ntz_partition_error(kernel_scan_file_paths(
5999 &unfiltered_scan,
6000 t_separator_source.table_uri(),
6001 ))?;
6002 assert_invalid_timestamp_ntz_partition_error(kernel_predicate_file_paths(
6003 &t_separator_source,
6004 timestamp_ntz_partition_eq(1_767_225_600_123_456),
6005 ))?;
6006
6007 let zone_table = DeltaLogTable::new_with_protocol_metadata_and_adds(
6008 "kernel-zone-timestamp-ntz-characterization",
6009 TIMESTAMP_NTZ_PROTOCOL_JSON,
6010 TIMESTAMP_NTZ_PARTITIONED_METADATA_JSON,
6011 &[
6012 partitioned_add_json(
6013 "timestamp-ntz-valid.parquet",
6014 r#"{"event_ts_ntz":"2026-01-01 00:00:00.123456"}"#,
6015 ),
6016 partitioned_add_json(
6017 "timestamp-ntz-zone.parquet",
6018 r#"{"event_ts_ntz":"2026-01-01T00:00:00.123456Z"}"#,
6019 ),
6020 ],
6021 )?;
6022 let zone_source = load_delta_source(DeltaSourceConfig {
6023 name: "orders".to_owned(),
6024 table_uri: zone_table.path.to_string_lossy().to_string(),
6025 version: None,
6026 storage_options: Default::default(),
6027 })?;
6028 let unfiltered_scan = build_projected_delta_scan(&zone_source, None)?;
6029
6030 assert_invalid_timestamp_ntz_partition_error(kernel_scan_file_paths(
6031 &unfiltered_scan,
6032 zone_source.table_uri(),
6033 ))?;
6034 assert_invalid_timestamp_ntz_partition_error(kernel_predicate_file_paths(
6035 &zone_source,
6036 timestamp_ntz_partition_eq(1_767_225_600_123_456),
6037 ))?;
6038
6039 Ok(())
6040 }
6041
6042 #[test]
6043 fn kernel_partition_characterization_documents_invalid_floating_metadata()
6044 -> Result<(), Box<dyn std::error::Error>> {
6045 let invalid_text_table = DeltaLogTable::new_with_metadata_and_adds(
6046 "kernel-invalid-floating-characterization",
6047 FLOATING_PARTITIONED_METADATA_JSON,
6048 &[
6049 partitioned_add_json(
6050 "floating-valid.parquet",
6051 r#"{"float_part":"1.5","double_part":"2.25"}"#,
6052 ),
6053 partitioned_add_json(
6054 "floating-invalid.parquet",
6055 r#"{"float_part":"not-a-float","double_part":"not-a-double"}"#,
6056 ),
6057 ],
6058 )?;
6059 let invalid_text_source = load_delta_source(DeltaSourceConfig {
6060 name: "orders".to_owned(),
6061 table_uri: invalid_text_table.path.to_string_lossy().to_string(),
6062 version: None,
6063 storage_options: Default::default(),
6064 })?;
6065 let unfiltered_scan = build_projected_delta_scan(&invalid_text_source, None)?;
6066
6067 assert_invalid_floating_partition_error(kernel_scan_file_paths(
6068 &unfiltered_scan,
6069 invalid_text_source.table_uri(),
6070 ))?;
6071 assert_invalid_floating_partition_error(kernel_predicated_file_paths(
6072 &invalid_text_source,
6073 &col("float_part").eq(float32_lit(1.5)),
6074 ))?;
6075
6076 Ok(())
6077 }
6078
6079 #[test]
6080 fn kernel_partition_characterization_documents_nonfinite_floating_metadata()
6081 -> Result<(), Box<dyn std::error::Error>> {
6082 let table = DeltaLogTable::new_with_metadata_and_adds(
6083 "kernel-nonfinite-floating-characterization",
6084 FLOATING_PARTITIONED_METADATA_JSON,
6085 &[
6086 partitioned_add_json(
6087 "floating-valid.parquet",
6088 r#"{"float_part":"1.5","double_part":"2.25"}"#,
6089 ),
6090 partitioned_add_json(
6091 "floating-nan.parquet",
6092 r#"{"float_part":"NaN","double_part":"NaN"}"#,
6093 ),
6094 partitioned_add_json(
6095 "floating-inf.parquet",
6096 r#"{"float_part":"Infinity","double_part":"Infinity"}"#,
6097 ),
6098 partitioned_add_json(
6099 "floating-neg-inf.parquet",
6100 r#"{"float_part":"-Infinity","double_part":"-Infinity"}"#,
6101 ),
6102 ],
6103 )?;
6104 let source = load_delta_source(DeltaSourceConfig {
6105 name: "orders".to_owned(),
6106 table_uri: table.path.to_string_lossy().to_string(),
6107 version: None,
6108 storage_options: Default::default(),
6109 })?;
6110 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
6111
6112 assert_eq!(
6113 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
6114 vec![
6115 "floating-inf.parquet",
6116 "floating-nan.parquet",
6117 "floating-neg-inf.parquet",
6118 "floating-valid.parquet",
6119 ]
6120 );
6121 assert_eq!(
6122 kernel_predicated_file_paths(&source, &col("float_part").gt(float32_lit(0.0)))?,
6123 vec![
6124 "floating-inf.parquet",
6125 "floating-nan.parquet",
6126 "floating-valid.parquet",
6127 ]
6128 );
6129 assert_eq!(
6130 kernel_predicated_file_paths(&source, &col("double_part").lt(float64_lit(0.0)))?,
6131 vec!["floating-neg-inf.parquet"]
6132 );
6133 assert_eq!(
6134 kernel_predicate_file_paths(
6135 &source,
6136 DeltaKernelPredicate::new(Predicate::eq(
6137 Expression::Column(ColumnName::new(["float_part"])),
6138 Expression::Literal(Scalar::Float(f32::NAN)),
6139 )),
6140 )?,
6141 vec!["floating-nan.parquet"]
6142 );
6143
6144 Ok(())
6145 }
6146
6147 #[test]
6148 fn kernel_partition_characterization_documents_invalid_date_metadata()
6149 -> Result<(), Box<dyn std::error::Error>> {
6150 let table = DeltaLogTable::new_with_metadata_and_adds(
6151 "kernel-invalid-date-characterization",
6152 DATE_PARTITIONED_METADATA_JSON,
6153 &[
6154 partitioned_add_json("date-valid.parquet", r#"{"event_date":"2026-01-01"}"#),
6155 partitioned_add_json("date-invalid.parquet", r#"{"event_date":"not-a-date"}"#),
6156 ],
6157 )?;
6158 let source = load_delta_source(DeltaSourceConfig {
6159 name: "orders".to_owned(),
6160 table_uri: table.path.to_string_lossy().to_string(),
6161 version: None,
6162 storage_options: Default::default(),
6163 })?;
6164 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
6165
6166 assert_invalid_date_partition_error(kernel_scan_file_paths(
6167 &unfiltered_scan,
6168 source.table_uri(),
6169 ))?;
6170 assert_invalid_date_partition_error(kernel_predicated_file_paths(
6171 &source,
6172 &col("event_date").eq(date_lit(20_454)),
6173 ))?;
6174
6175 Ok(())
6176 }
6177
6178 #[test]
6179 fn kernel_partition_characterization_documents_invalid_boolean_metadata()
6180 -> Result<(), Box<dyn std::error::Error>> {
6181 let table = DeltaLogTable::new_with_metadata_and_adds(
6182 "kernel-invalid-boolean-characterization",
6183 BOOLEAN_PARTITIONED_METADATA_JSON,
6184 &[
6185 partitioned_add_json("boolean-valid.parquet", r#"{"is_current":"true"}"#),
6186 partitioned_add_json(
6187 "boolean-invalid.parquet",
6188 r#"{"is_current":"not-a-boolean"}"#,
6189 ),
6190 ],
6191 )?;
6192 let source = load_delta_source(DeltaSourceConfig {
6193 name: "orders".to_owned(),
6194 table_uri: table.path.to_string_lossy().to_string(),
6195 version: None,
6196 storage_options: Default::default(),
6197 })?;
6198 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
6199
6200 assert_invalid_boolean_partition_error(kernel_scan_file_paths(
6201 &unfiltered_scan,
6202 source.table_uri(),
6203 ))?;
6204 assert_invalid_boolean_partition_error(kernel_predicated_file_paths(
6205 &source,
6206 &col("is_current").eq(bool_lit(true)),
6207 ))?;
6208
6209 Ok(())
6210 }
6211
6212 #[test]
6213 fn kernel_partition_characterization_documents_integer_numeric_ordering()
6214 -> Result<(), Box<dyn std::error::Error>> {
6215 let (_table, source) = kernel_integer_partition_characterization_source()?;
6216 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
6217
6218 assert_eq!(
6219 kernel_scan_file_paths(&unfiltered_scan, source.table_uri())?,
6220 vec![
6221 "integer--1.parquet",
6222 "integer-10.parquet",
6223 "integer-2.parquet",
6224 "integer-empty.parquet",
6225 "integer-missing.parquet",
6226 "integer-null.parquet",
6227 ]
6228 );
6229 assert_eq!(
6230 kernel_predicated_file_paths(&source, &col("long_part").gt(int64_lit(2)))?,
6231 vec!["integer-10.parquet"]
6232 );
6233 assert_eq!(
6234 kernel_predicated_file_paths(&source, &col("long_part").lt(int64_lit(10)))?,
6235 vec!["integer--1.parquet", "integer-2.parquet"]
6236 );
6237 assert_eq!(
6238 kernel_predicated_file_paths(&source, &int64_lit(10).gt(col("long_part")))?,
6239 vec!["integer--1.parquet", "integer-2.parquet"]
6240 );
6241
6242 Ok(())
6243 }
6244
6245 #[test]
6246 fn kernel_partition_characterization_documents_integer_null_and_empty_semantics()
6247 -> Result<(), Box<dyn std::error::Error>> {
6248 let (_table, source) = kernel_integer_partition_characterization_source()?;
6249
6250 assert_eq!(
6251 kernel_predicated_file_paths(&source, &col("long_part").is_null())?,
6252 vec![
6253 "integer-empty.parquet",
6254 "integer-missing.parquet",
6255 "integer-null.parquet",
6256 ]
6257 );
6258 assert_eq!(
6259 kernel_predicated_file_paths(&source, &col("long_part").is_not_null())?,
6260 vec![
6261 "integer--1.parquet",
6262 "integer-10.parquet",
6263 "integer-2.parquet",
6264 ]
6265 );
6266 assert_eq!(
6267 kernel_predicated_file_paths(&source, &col("long_part").eq(int64_lit(2)))?,
6268 vec!["integer-2.parquet"]
6269 );
6270 assert_eq!(
6271 kernel_predicated_file_paths(&source, &col("long_part").not_eq(int64_lit(2)))?,
6272 vec!["integer--1.parquet", "integer-10.parquet"]
6273 );
6274
6275 Ok(())
6276 }
6277
6278 #[test]
6279 fn kernel_partition_characterization_documents_integer_membership_between_and_composition()
6280 -> Result<(), Box<dyn std::error::Error>> {
6281 let (_table, source) = kernel_integer_partition_characterization_source()?;
6282
6283 assert_eq!(
6284 kernel_predicated_file_paths(
6285 &source,
6286 &col("long_part").in_list(vec![int64_lit(-1), int64_lit(10)], false),
6287 )?,
6288 vec!["integer--1.parquet", "integer-10.parquet"]
6289 );
6290 assert_eq!(
6291 kernel_predicated_file_paths(
6292 &source,
6293 &col("long_part").in_list(vec![int64_lit(2)], true),
6294 )?,
6295 vec!["integer--1.parquet", "integer-10.parquet"]
6296 );
6297 assert_eq!(
6298 kernel_predicated_file_paths(
6299 &source,
6300 &col("long_part").between(int64_lit(-1), int64_lit(2)),
6301 )?,
6302 vec!["integer--1.parquet", "integer-2.parquet"]
6303 );
6304 assert_eq!(
6305 kernel_predicated_file_paths(
6306 &source,
6307 &col("long_part").not_between(int64_lit(-1), int64_lit(2)),
6308 )?,
6309 vec!["integer-10.parquet"]
6310 );
6311 assert_eq!(
6312 kernel_predicated_file_paths(
6313 &source,
6314 &col("long_part")
6315 .gt_eq(int64_lit(-1))
6316 .and(col("long_part").lt(int64_lit(10))),
6317 )?,
6318 vec!["integer--1.parquet", "integer-2.parquet"]
6319 );
6320 assert_eq!(
6321 kernel_predicated_file_paths(
6322 &source,
6323 &col("long_part")
6324 .eq(int64_lit(-1))
6325 .or(col("long_part").eq(int64_lit(10))),
6326 )?,
6327 vec!["integer--1.parquet", "integer-10.parquet"]
6328 );
6329 assert_eq!(
6330 kernel_predicated_file_paths(
6331 &source,
6332 &Expr::Not(Box::new(col("long_part").eq(int64_lit(2)))),
6333 )?,
6334 vec!["integer--1.parquet", "integer-10.parquet"]
6335 );
6336
6337 Ok(())
6338 }
6339
6340 #[test]
6341 fn kernel_partition_characterization_documents_integer_width_scalars()
6342 -> Result<(), Box<dyn std::error::Error>> {
6343 let table = DeltaLogTable::new_with_metadata_and_adds(
6344 "kernel-integer-width-characterization",
6345 INTEGER_PARTITIONED_METADATA_JSON,
6346 &[
6347 partitioned_add_json(
6348 "width-byte-min.parquet",
6349 r#"{"byte_part":"-128","short_part":"0","int_part":"0","long_part":"0"}"#,
6350 ),
6351 partitioned_add_json(
6352 "width-short-max.parquet",
6353 r#"{"byte_part":"0","short_part":"32767","int_part":"0","long_part":"0"}"#,
6354 ),
6355 partitioned_add_json(
6356 "width-int-max.parquet",
6357 r#"{"byte_part":"0","short_part":"0","int_part":"2147483647","long_part":"0"}"#,
6358 ),
6359 partitioned_add_json(
6360 "width-long-max.parquet",
6361 r#"{"byte_part":"0","short_part":"0","int_part":"0","long_part":"9223372036854775807"}"#,
6362 ),
6363 ],
6364 )?;
6365 let source = load_delta_source(DeltaSourceConfig {
6366 name: "orders".to_owned(),
6367 table_uri: table.path.to_string_lossy().to_string(),
6368 version: None,
6369 storage_options: Default::default(),
6370 })?;
6371
6372 assert_eq!(
6373 kernel_predicated_file_paths(&source, &col("byte_part").eq(int8_lit(i8::MIN)))?,
6374 vec!["width-byte-min.parquet"]
6375 );
6376 assert_eq!(
6377 kernel_predicated_file_paths(&source, &col("short_part").eq(int16_lit(i16::MAX)))?,
6378 vec!["width-short-max.parquet"]
6379 );
6380 assert_eq!(
6381 kernel_predicated_file_paths(&source, &col("int_part").eq(int32_lit(i32::MAX)))?,
6382 vec!["width-int-max.parquet"]
6383 );
6384 assert_eq!(
6385 kernel_predicated_file_paths(&source, &col("long_part").eq(int64_lit(i64::MAX)))?,
6386 vec!["width-long-max.parquet"]
6387 );
6388
6389 Ok(())
6390 }
6391
6392 #[test]
6393 fn kernel_partition_characterization_documents_invalid_integer_metadata()
6394 -> Result<(), Box<dyn std::error::Error>> {
6395 let table = DeltaLogTable::new_with_metadata_and_adds(
6396 "kernel-invalid-integer-characterization",
6397 INTEGER_PARTITIONED_METADATA_JSON,
6398 &[
6399 integer_partitioned_add_json("integer-valid.parquet", "7"),
6400 partitioned_add_json(
6401 "integer-invalid.parquet",
6402 r#"{"byte_part":"0","short_part":"0","int_part":"0","long_part":"not-an-integer"}"#,
6403 ),
6404 ],
6405 )?;
6406 let source = load_delta_source(DeltaSourceConfig {
6407 name: "orders".to_owned(),
6408 table_uri: table.path.to_string_lossy().to_string(),
6409 version: None,
6410 storage_options: Default::default(),
6411 })?;
6412 let unfiltered_scan = build_projected_delta_scan(&source, None)?;
6413
6414 assert_invalid_integer_partition_error(kernel_scan_file_paths(
6415 &unfiltered_scan,
6416 source.table_uri(),
6417 ))?;
6418 assert_invalid_integer_partition_error(kernel_predicated_file_paths(
6419 &source,
6420 &col("long_part").eq(int64_lit(7)),
6421 ))?;
6422
6423 Ok(())
6424 }
6425
6426 #[test]
6427 fn loads_named_delta_source() -> Result<(), Box<dyn std::error::Error>> {
6428 let table = DeltaLogTable::new("success")?;
6429 let requested_table_uri = table.path.to_string_lossy().to_string();
6430
6431 let source = load_delta_source(DeltaSourceConfig {
6432 name: "orders".to_owned(),
6433 table_uri: requested_table_uri.clone(),
6434 version: None,
6435 storage_options: Default::default(),
6436 })?;
6437
6438 assert_eq!(source.name(), "orders");
6439 assert_eq!(source.requested_table_uri(), requested_table_uri);
6440 assert!(source.table_uri().starts_with("file://"));
6441 assert_eq!(source.version(), 1);
6442 assert_eq!(source.loaded_snapshot().version(), 1);
6443
6444 Ok(())
6445 }
6446
6447 #[test]
6448 fn planned_delta_source_owns_and_isolates_storage_options()
6449 -> Result<(), Box<dyn std::error::Error>> {
6450 let table = DeltaLogTable::new("storage-options-owned")?;
6451 let mut caller_options = storage_options(&[
6452 ("authorization", "caller-original"),
6453 ("region", "us-west-2"),
6454 ]);
6455 let source = load_delta_source(
6456 DeltaSourceConfig::new("orders", table.path.to_string_lossy())
6457 .with_storage_options(caller_options.clone()),
6458 )?;
6459
6460 caller_options.insert("authorization".to_owned(), "caller-mutated".to_owned());
6461 caller_options.insert("new-secret".to_owned(), "caller-added".to_owned());
6462
6463 assert_eq!(
6464 source
6465 .storage_options()
6466 .get("authorization")
6467 .map(String::as_str),
6468 Some("caller-original")
6469 );
6470 assert_eq!(
6471 source.storage_options().get("region").map(String::as_str),
6472 Some("us-west-2")
6473 );
6474 assert!(!source.storage_options().contains_key("new-secret"));
6475
6476 Ok(())
6477 }
6478
6479 #[test]
6480 fn s3_effective_storage_options_merge_aws_env_when_absent() {
6481 for table_uri in [
6482 "s3://bucket/root",
6483 "s3a://bucket/root",
6484 "https://s3.us-east-1.amazonaws.com/bucket/root",
6485 "https://bucket.s3.us-east-1.amazonaws.com/root",
6486 "https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket/root",
6487 ] {
6488 let effective = effective_storage_options_for_source_from_env(
6489 table_uri,
6490 DeltaStorageOptions::default(),
6491 env_vars(&[
6492 ("AWS_ACCESS_KEY_ID", "env-access"),
6493 ("AWS_SECRET_ACCESS_KEY", "env-secret"),
6494 ("AWS_REGION", "us-east-1"),
6495 ("AWS_CUSTOM_FUTURE_OPTION", "future"),
6496 ("NOT_AWS", "ignored"),
6497 ]),
6498 );
6499
6500 assert_eq!(
6501 effective.get("AWS_ACCESS_KEY_ID").map(String::as_str),
6502 Some("env-access"),
6503 "{table_uri}"
6504 );
6505 assert_eq!(
6506 effective.get("AWS_SECRET_ACCESS_KEY").map(String::as_str),
6507 Some("env-secret"),
6508 "{table_uri}"
6509 );
6510 assert_eq!(
6511 effective.get("AWS_REGION").map(String::as_str),
6512 Some("us-east-1"),
6513 "{table_uri}"
6514 );
6515 assert_eq!(
6516 effective
6517 .get("AWS_CUSTOM_FUTURE_OPTION")
6518 .map(String::as_str),
6519 Some("future"),
6520 "{table_uri}"
6521 );
6522 assert!(!effective.contains_key("NOT_AWS"), "{table_uri}");
6523 }
6524 }
6525
6526 #[test]
6527 fn s3_effective_storage_options_prefer_explicit_caller_options() {
6528 let caller_options = storage_options(&[
6529 ("aws_access_key_id", "caller-access"),
6530 ("AWS_SECRET_ACCESS_KEY", "caller-secret"),
6531 ("region", "caller-region"),
6532 ]);
6533 let effective = effective_storage_options_for_source_from_env(
6534 "s3://bucket/root",
6535 caller_options,
6536 env_vars(&[
6537 ("AWS_ACCESS_KEY_ID", "env-access"),
6538 ("AWS_SECRET_ACCESS_KEY", "env-secret"),
6539 ("AWS_REGION", "env-region"),
6540 ("AWS_CUSTOM_FUTURE_OPTION", "future"),
6541 ]),
6542 );
6543
6544 assert_eq!(
6545 effective.get("aws_access_key_id").map(String::as_str),
6546 Some("caller-access")
6547 );
6548 assert_eq!(
6549 effective.get("AWS_SECRET_ACCESS_KEY").map(String::as_str),
6550 Some("caller-secret")
6551 );
6552 assert_eq!(
6553 effective.get("region").map(String::as_str),
6554 Some("caller-region")
6555 );
6556 assert_eq!(
6557 effective
6558 .get("AWS_CUSTOM_FUTURE_OPTION")
6559 .map(String::as_str),
6560 Some("future")
6561 );
6562 assert!(!effective.contains_key("AWS_ACCESS_KEY_ID"));
6563 assert!(!effective.contains_key("AWS_REGION"));
6564 }
6565
6566 #[test]
6567 fn non_s3_effective_storage_options_do_not_merge_aws_env() {
6568 let caller_options = storage_options(&[("authorization", "caller-token")]);
6569 let effective = effective_storage_options_for_source_from_env(
6570 "file:///tmp/table",
6571 caller_options.clone(),
6572 env_vars(&[
6573 ("AWS_ACCESS_KEY_ID", "env-access"),
6574 ("AWS_SECRET_ACCESS_KEY", "env-secret"),
6575 ("AWS_REGION", "us-east-1"),
6576 ]),
6577 );
6578
6579 assert_eq!(effective, caller_options);
6580 }
6581
6582 #[test]
6583 fn s3_effective_storage_options_are_resolved_before_source_load() {
6584 let storage_options = effective_storage_options_for_source_from_env(
6585 "s3://bucket/root",
6586 storage_options(&[("region", "caller-region")]),
6587 env_vars(&[
6588 ("AWS_ACCESS_KEY_ID", "env-access"),
6589 ("AWS_SECRET_ACCESS_KEY", "env-secret"),
6590 ("AWS_REGION", "env-region"),
6591 ]),
6592 );
6593
6594 assert_eq!(
6595 storage_options.get("AWS_ACCESS_KEY_ID").map(String::as_str),
6596 Some("env-access")
6597 );
6598 assert_eq!(
6599 storage_options
6600 .get("AWS_SECRET_ACCESS_KEY")
6601 .map(String::as_str),
6602 Some("env-secret")
6603 );
6604 assert_eq!(
6605 storage_options.get("region").map(String::as_str),
6606 Some("caller-region")
6607 );
6608 assert!(!storage_options.contains_key("AWS_REGION"));
6609 }
6610
6611 #[test]
6612 fn s3_storage_options_accept_documented_credential_keys() {
6613 for (option_key, config_key, expected_value) in [
6614 (
6615 "AWS_ACCESS_KEY_ID",
6616 AmazonS3ConfigKey::AccessKeyId,
6617 "upper-access",
6618 ),
6619 (
6620 "aws_access_key_id",
6621 AmazonS3ConfigKey::AccessKeyId,
6622 "lower-access",
6623 ),
6624 (
6625 "AWS_SECRET_ACCESS_KEY",
6626 AmazonS3ConfigKey::SecretAccessKey,
6627 "upper-secret",
6628 ),
6629 (
6630 "aws_secret_access_key",
6631 AmazonS3ConfigKey::SecretAccessKey,
6632 "lower-secret",
6633 ),
6634 ("AWS_SESSION_TOKEN", AmazonS3ConfigKey::Token, "upper-token"),
6635 ("aws_session_token", AmazonS3ConfigKey::Token, "lower-token"),
6636 ] {
6637 let options = storage_options(&[(option_key, expected_value)]);
6638 let builder = s3_builder_from_storage_options(&options);
6639
6640 assert_eq!(
6641 builder.get_config_value(&config_key).as_deref(),
6642 Some(expected_value),
6643 "{option_key}"
6644 );
6645 }
6646 }
6647
6648 #[test]
6649 fn s3_storage_options_accept_documented_region_keys() {
6650 for option_key in ["AWS_REGION", "aws_region", "region"] {
6651 let options = storage_options(&[(option_key, "explicit-region")]);
6652 let builder = s3_builder_from_storage_options(&options);
6653
6654 assert_eq!(
6655 builder
6656 .get_config_value(&AmazonS3ConfigKey::Region)
6657 .as_deref(),
6658 Some("explicit-region"),
6659 "{option_key}"
6660 );
6661 }
6662
6663 for option_key in ["AWS_DEFAULT_REGION", "aws_default_region"] {
6664 let options = storage_options(&[(option_key, "fallback-region")]);
6665 let builder = s3_builder_from_storage_options(&options);
6666
6667 assert_eq!(
6668 builder
6669 .get_config_value(&AmazonS3ConfigKey::Region)
6670 .as_deref(),
6671 Some("fallback-region"),
6672 "{option_key}"
6673 );
6674 }
6675 }
6676
6677 #[test]
6678 fn s3_default_region_only_applies_without_explicit_region() {
6679 for default_key in ["AWS_DEFAULT_REGION", "aws_default_region"] {
6680 for explicit_key in ["AWS_REGION", "aws_region", "region"] {
6681 let options = storage_options(&[
6682 (default_key, "fallback-region"),
6683 (explicit_key, "explicit-region"),
6684 ]);
6685 let builder = s3_builder_from_storage_options(&options);
6686
6687 assert_eq!(
6688 builder
6689 .get_config_value(&AmazonS3ConfigKey::Region)
6690 .as_deref(),
6691 Some("explicit-region"),
6692 "{default_key} with {explicit_key}"
6693 );
6694 }
6695 }
6696 }
6697
6698 #[test]
6699 fn s3_store_construction_accepts_documented_storage_option_keys()
6700 -> Result<(), Box<dyn std::error::Error>> {
6701 let cases = [
6702 (
6703 "AWS_ACCESS_KEY_ID",
6704 "AWS_SECRET_ACCESS_KEY",
6705 "AWS_SESSION_TOKEN",
6706 "AWS_REGION",
6707 ),
6708 (
6709 "aws_access_key_id",
6710 "aws_secret_access_key",
6711 "aws_session_token",
6712 "aws_region",
6713 ),
6714 (
6715 "aws_access_key_id",
6716 "aws_secret_access_key",
6717 "aws_session_token",
6718 "region",
6719 ),
6720 (
6721 "aws_access_key_id",
6722 "aws_secret_access_key",
6723 "aws_session_token",
6724 "AWS_DEFAULT_REGION",
6725 ),
6726 (
6727 "aws_access_key_id",
6728 "aws_secret_access_key",
6729 "aws_session_token",
6730 "aws_default_region",
6731 ),
6732 ];
6733 let table_url = super::kernel::try_parse_uri("s3://bucket/root")?;
6734
6735 for (access_key, secret_key, token_key, region_key) in cases {
6736 let options = storage_options(&[
6737 (access_key, "access"),
6738 (secret_key, "secret"),
6739 (token_key, "token"),
6740 (region_key, "us-east-1"),
6741 ]);
6742
6743 let _store = super::kernel::store_from_url_opts(
6744 &table_url,
6745 options
6746 .iter()
6747 .map(|(key, value)| (key.as_str(), value.as_str())),
6748 )?;
6749 }
6750
6751 Ok(())
6752 }
6753
6754 #[test]
6755 fn source_loading_passes_storage_options_to_kernel_store_construction()
6756 -> Result<(), Box<dyn std::error::Error>> {
6757 let scheme = unique_storage_scheme("sourceoptions")?;
6758 let captured = CapturedStorageOptions::default();
6759 register_capturing_storage_handler(&scheme, Arc::clone(&captured))?;
6760 let options =
6761 storage_options(&[("authorization", "source-token"), ("region", "us-east-1")]);
6762
6763 let result = load_delta_source(
6764 DeltaSourceConfig::new("orders", format!("{scheme}://table/root"))
6765 .with_storage_options(options.clone()),
6766 );
6767
6768 assert!(matches!(
6769 result,
6770 Err(DeltaFunnelError::DeltaSnapshotLoad { .. })
6771 ));
6772 assert_eq!(captured_storage_options(&captured), vec![options]);
6773
6774 Ok(())
6775 }
6776
6777 #[test]
6778 fn reader_construction_passes_storage_options_to_each_store_construction()
6779 -> Result<(), Box<dyn std::error::Error>> {
6780 let scheme = unique_storage_scheme("readeroptions")?;
6781 let captured = CapturedStorageOptions::default();
6782 register_capturing_storage_handler(&scheme, Arc::clone(&captured))?;
6783 let table_uri = format!("{scheme}://table/root/");
6784 let options = storage_options(&[
6785 ("authorization", "reader-token"),
6786 ("endpoint", "http://storage.example"),
6787 ]);
6788
6789 let _data_reader = KernelDataFileReader::try_new(KernelDataFileReaderConfig {
6790 source_name: "orders",
6791 table_uri: &table_uri,
6792 snapshot_version: 42,
6793 storage_options: &options,
6794 })?;
6795 let _dv_reader = KernelDeletionVectorReader::try_new(KernelDeletionVectorReaderConfig {
6796 source_name: "orders",
6797 table_uri: &table_uri,
6798 snapshot_version: 42,
6799 storage_options: &options,
6800 })?;
6801 let captured_options = captured_storage_options(&captured);
6802 assert_eq!(captured_options.len(), 2);
6803 assert!(captured_options.iter().all(|captured| captured == &options));
6804
6805 Ok(())
6806 }
6807
6808 #[test]
6809 fn reader_construction_passes_effective_s3_storage_options_to_each_store_construction()
6810 -> Result<(), Box<dyn std::error::Error>> {
6811 let scheme = unique_storage_scheme("readereffectiveoptions")?;
6812 let captured = CapturedStorageOptions::default();
6813 register_capturing_storage_handler(&scheme, Arc::clone(&captured))?;
6814 let table_uri = format!("{scheme}://table/root/");
6815 let effective = effective_storage_options_for_source_from_env(
6816 "s3://bucket/root",
6817 storage_options(&[("region", "caller-region")]),
6818 env_vars(&[
6819 ("AWS_ACCESS_KEY_ID", "env-access"),
6820 ("AWS_SECRET_ACCESS_KEY", "env-secret"),
6821 ("AWS_REGION", "env-region"),
6822 ]),
6823 );
6824
6825 let _data_reader = KernelDataFileReader::try_new(KernelDataFileReaderConfig {
6826 source_name: "orders",
6827 table_uri: &table_uri,
6828 snapshot_version: 42,
6829 storage_options: &effective,
6830 })?;
6831 let _dv_reader = KernelDeletionVectorReader::try_new(KernelDeletionVectorReaderConfig {
6832 source_name: "orders",
6833 table_uri: &table_uri,
6834 snapshot_version: 42,
6835 storage_options: &effective,
6836 })?;
6837 let captured_options = captured_storage_options(&captured);
6838 assert_eq!(captured_options.len(), 2);
6839 assert!(
6840 captured_options
6841 .iter()
6842 .all(|captured| captured == &effective)
6843 );
6844
6845 Ok(())
6846 }
6847
6848 #[test]
6849 fn storage_option_values_are_not_exposed_when_store_construction_fails()
6850 -> Result<(), Box<dyn std::error::Error>> {
6851 for secret_key in [
6852 "AWS_SECRET_ACCESS_KEY",
6853 "aws_secret_access_key",
6854 "AWS_SESSION_TOKEN",
6855 "aws_session_token",
6856 ] {
6857 let scheme = unique_storage_scheme("redactedoptions")?;
6858 let captured = CapturedStorageOptions::default();
6859 let secret_value = "super-secret-token-value";
6860 register_failing_storage_handler(&scheme, Arc::clone(&captured), secret_value)?;
6861 let options =
6862 storage_options(&[(secret_key, secret_value), ("AWS_REGION", "us-west-2")]);
6863
6864 let result = load_delta_source(
6865 DeltaSourceConfig::new("orders", format!("{scheme}://table/root"))
6866 .with_storage_options(options.clone()),
6867 );
6868 let error = result
6869 .err()
6870 .map(|error| error.to_string())
6871 .unwrap_or_default();
6872
6873 assert_eq!(captured_storage_options(&captured), vec![options]);
6874 assert!(!error.contains(secret_value), "{secret_key}");
6875 assert!(!error.contains(secret_key), "{secret_key}");
6876 }
6877
6878 Ok(())
6879 }
6880
6881 #[test]
6882 fn merged_s3_env_storage_option_values_are_not_exposed_when_store_construction_fails()
6883 -> Result<(), Box<dyn std::error::Error>> {
6884 let scheme = unique_storage_scheme("redacteds3envoptions")?;
6885 let captured = CapturedStorageOptions::default();
6886 let secret_value = "super-secret-env-token-value";
6887 register_failing_storage_handler(&scheme, Arc::clone(&captured), secret_value)?;
6888 let effective = effective_storage_options_for_source_from_env(
6889 "s3://bucket/root",
6890 DeltaStorageOptions::default(),
6891 env_vars(&[
6892 ("AWS_ACCESS_KEY_ID", "env-access"),
6893 ("AWS_SECRET_ACCESS_KEY", secret_value),
6894 ("AWS_REGION", "us-east-1"),
6895 ]),
6896 );
6897
6898 let result = load_delta_source(
6899 DeltaSourceConfig::new("orders", format!("{scheme}://table/root"))
6900 .with_storage_options(effective.clone()),
6901 );
6902 let error = result
6903 .err()
6904 .map(|error| error.to_string())
6905 .unwrap_or_default();
6906
6907 assert_eq!(captured_storage_options(&captured), vec![effective]);
6908 assert!(!error.contains(secret_value));
6909 assert!(!error.contains("AWS_SECRET_ACCESS_KEY"));
6910
6911 Ok(())
6912 }
6913
6914 #[test]
6915 fn invalid_recognized_s3_aws_env_values_fail_store_construction() {
6916 let storage_options = effective_storage_options_for_source_from_env(
6917 "s3://bucket/root",
6918 DeltaStorageOptions::default(),
6919 env_vars(&[
6920 ("AWS_ALLOW_HTTP", "not-a-bool"),
6921 ("AWS_REGION", "us-east-1"),
6922 ]),
6923 );
6924
6925 let result = load_delta_table_snapshot("s3://bucket/root", None, &storage_options);
6926
6927 assert!(matches!(
6928 result,
6929 Err(DeltaFunnelError::DeltaSourceEngine { .. })
6930 ));
6931 }
6932
6933 #[test]
6934 fn loads_named_delta_source_at_fixed_version() -> Result<(), Box<dyn std::error::Error>> {
6935 let table = DeltaLogTable::new("fixed")?;
6936
6937 let source = load_delta_source(DeltaSourceConfig {
6938 name: "orders".to_owned(),
6939 table_uri: table.path.to_string_lossy().to_string(),
6940 version: Some(0),
6941 storage_options: Default::default(),
6942 })?;
6943
6944 assert_eq!(source.version(), 0);
6945
6946 Ok(())
6947 }
6948
6949 #[test]
6950 fn validates_name_before_table_uri() {
6951 let result = load_delta_source(DeltaSourceConfig {
6952 name: "orders.latest".to_owned(),
6953 table_uri: "missing/path".to_owned(),
6954 version: None,
6955 storage_options: Default::default(),
6956 });
6957
6958 assert!(matches!(
6959 result,
6960 Err(DeltaFunnelError::InvalidSourceName { .. })
6961 ));
6962 }
6963
6964 #[test]
6965 fn rejects_sql_keyword_name_before_table_uri() {
6966 let result = load_delta_source(DeltaSourceConfig {
6967 name: "select".to_owned(),
6968 table_uri: "missing/path".to_owned(),
6969 version: None,
6970 storage_options: Default::default(),
6971 });
6972
6973 assert!(matches!(
6974 result,
6975 Err(DeltaFunnelError::InvalidSourceName { name, reason })
6976 if name == "select" && reason == "source names must not be SQL keywords"
6977 ));
6978 }
6979
6980 #[test]
6981 fn rejects_blank_table_uri_as_invalid_source_uri() {
6982 let result = load_delta_source(DeltaSourceConfig {
6983 name: "orders".to_owned(),
6984 table_uri: " \t\n".to_owned(),
6985 version: None,
6986 storage_options: Default::default(),
6987 });
6988
6989 assert!(matches!(
6990 result,
6991 Err(DeltaFunnelError::InvalidSourceUri { reason })
6992 if reason == "table location must not be empty"
6993 ));
6994 }
6995
6996 #[test]
6997 fn loads_multiple_named_delta_sources() -> Result<(), Box<dyn std::error::Error>> {
6998 let orders = DeltaLogTable::new("orders")?;
6999 let customers = DeltaLogTable::new("customers")?;
7000
7001 let sources = load_delta_sources([
7002 DeltaSourceConfig {
7003 name: "orders".to_owned(),
7004 table_uri: orders.path.to_string_lossy().to_string(),
7005 version: None,
7006 storage_options: Default::default(),
7007 },
7008 DeltaSourceConfig {
7009 name: "customers".to_owned(),
7010 table_uri: customers.path.to_string_lossy().to_string(),
7011 version: Some(0),
7012 storage_options: Default::default(),
7013 },
7014 ])?;
7015
7016 assert_eq!(sources.len(), 2);
7017 assert_eq!(sources[0].name(), "orders");
7018 assert_eq!(sources[0].version(), 1);
7019 assert_eq!(sources[1].name(), "customers");
7020 assert_eq!(sources[1].version(), 0);
7021
7022 Ok(())
7023 }
7024
7025 #[test]
7026 fn rejects_duplicate_names_before_table_uri_loading() {
7027 let result = load_delta_sources([
7028 DeltaSourceConfig {
7029 name: "orders".to_owned(),
7030 table_uri: "missing/orders".to_owned(),
7031 version: None,
7032 storage_options: Default::default(),
7033 },
7034 DeltaSourceConfig {
7035 name: "Orders".to_owned(),
7036 table_uri: "missing/customers".to_owned(),
7037 version: None,
7038 storage_options: Default::default(),
7039 },
7040 ]);
7041
7042 assert!(matches!(
7043 result,
7044 Err(DeltaFunnelError::DuplicateSourceName { name }) if name == "Orders"
7045 ));
7046 }
7047
7048 #[test]
7049 fn rejects_invalid_name_before_multi_source_table_uri_loading() {
7050 let result = load_delta_sources([
7051 DeltaSourceConfig {
7052 name: "orders".to_owned(),
7053 table_uri: "missing/orders".to_owned(),
7054 version: None,
7055 storage_options: Default::default(),
7056 },
7057 DeltaSourceConfig {
7058 name: "customers.latest".to_owned(),
7059 table_uri: "missing/customers".to_owned(),
7060 version: None,
7061 storage_options: Default::default(),
7062 },
7063 ]);
7064
7065 assert!(matches!(
7066 result,
7067 Err(DeltaFunnelError::InvalidSourceName { name, .. }) if name == "customers.latest"
7068 ));
7069 }
7070
7071 #[test]
7072 fn source_load_errors_do_not_expose_secret_bearing_uri() {
7073 let result = load_delta_source(DeltaSourceConfig {
7074 name: "orders".to_owned(),
7075 table_uri: "ftp://user:password@example.com/table".to_owned(),
7076 version: None,
7077 storage_options: Default::default(),
7078 });
7079
7080 let error = result
7081 .err()
7082 .map(|error| error.to_string())
7083 .unwrap_or_default();
7084
7085 assert!(!error.contains("user"));
7086 assert!(!error.contains("password"));
7087 assert!(!error.contains("example.com"));
7088 assert!(!error.contains("ftp://"));
7089 }
7090}