Skip to main content

delta_funnel/table_formats/
delta.rs

1//! Delta table-format source loading.
2
3use 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/// Metadata-only expansion of one kernel scan.
43///
44/// This is the direct output of `delta_kernel` scan metadata expansion. It is
45/// not the provider execution task model; provider planning converts
46/// these file metadata records into Delta-aware file tasks and grouped scan
47/// partitions.
48#[allow(dead_code)]
49pub(crate) struct KernelScanMetadataExpansion {
50    /// File-level metadata rows yielded by Delta Kernel scan metadata expansion.
51    pub(crate) files: Vec<KernelScanFileMetadata>,
52    /// Whether the kernel scan metadata iterator was consumed to completion.
53    ///
54    /// This records that the adapter did not stop after a partial metadata
55    /// batch. Later planning layers can rely on `files` representing the full
56    /// kernel-selected file set for this scan.
57    pub(crate) scan_metadata_exhausted: bool,
58}
59
60/// Metadata for one file selected by kernel scan planning.
61#[allow(dead_code)]
62pub(crate) struct KernelScanFileMetadata {
63    /// Delta add-action path for the selected data file.
64    ///
65    /// This is the table-relative path that Delta Kernel exposes in
66    /// `ScanFile`, not a resolved object-store URI.
67    pub(crate) path: String,
68    /// File size in bytes from the Delta add action.
69    pub(crate) size: i64,
70    /// Last modification timestamp from the Delta add action.
71    ///
72    /// Delta log metadata stores this value in milliseconds since the Unix
73    /// epoch.
74    pub(crate) modification_time: i64,
75    /// Parsed file statistics from Delta Kernel when stats are available.
76    pub(crate) stats: Option<KernelScanFileStats>,
77    /// Deletion-vector metadata selected with this file, if one exists.
78    ///
79    /// The handle intentionally preserves kernel-owned metadata only. It must
80    /// not read or materialize the deletion-vector payload during metadata
81    /// expansion.
82    pub(crate) deletion_vector: KernelScanDeletionVectorMetadata,
83    /// Optional physical-to-logical transform required for this file.
84    ///
85    /// Delta Kernel uses this for features where physical Parquet columns do
86    /// not directly match the logical scan schema. The expression is preserved
87    /// for provider execution and is not evaluated here.
88    pub(crate) physical_to_logical_transform: KernelPhysicalToLogicalTransform,
89    /// Partition values from the Delta add action keyed by partition column.
90    ///
91    /// Values are the raw string metadata values exposed by Delta Kernel.
92    pub(crate) partition_values: HashMap<String, String>,
93}
94
95/// Parsed statistics for one kernel-selected scan file.
96#[allow(dead_code)]
97pub(crate) struct KernelScanFileStats {
98    /// Number of records parsed from the Delta add-action stats JSON.
99    pub(crate) num_records: u64,
100}
101
102/// Opaque metadata for a deletion vector selected by kernel scan planning.
103///
104/// This preserves the kernel-owned deletion-vector descriptor for provider
105/// execution without loading the deletion-vector payload.
106#[allow(dead_code)]
107#[derive(Clone)]
108pub(crate) enum KernelScanDeletionVectorMetadata {
109    /// The selected file has no deletion vector.
110    NotPresent,
111    /// The selected file has deletion-vector metadata preserved for execution.
112    Present(KernelScanDeletionVectorHandle),
113}
114
115/// Opaque handle for kernel-owned deletion-vector metadata.
116///
117/// The wrapped `DvInfo` is kept private so metadata expansion cannot
118/// accidentally load the deletion-vector payload. Execution code must load
119/// payloads through the private deletion-vector reader boundary.
120#[allow(dead_code)]
121#[derive(Clone)]
122pub(crate) struct KernelScanDeletionVectorHandle {
123    /// Kernel deletion-vector metadata for the selected file.
124    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/// Opaque physical-to-logical transform metadata selected for one scan file.
152///
153/// The transform is applied only after physical Parquet data has been read,
154/// which belongs to provider execution rather than metadata expansion.
155#[allow(dead_code)]
156#[derive(Clone)]
157pub(crate) enum KernelPhysicalToLogicalTransform {
158    /// The physical Parquet file already matches the logical scan shape.
159    NotRequired,
160    /// A kernel expression must be applied after physical data is read.
161    Required(KernelPhysicalToLogicalTransformHandle),
162}
163
164/// Opaque handle for a kernel physical-to-logical transform expression.
165///
166/// The expression is metadata for provider execution. It is not evaluated while
167/// expanding scan metadata.
168#[allow(dead_code)]
169#[derive(Clone)]
170pub(crate) struct KernelPhysicalToLogicalTransformHandle {
171    /// Kernel expression that maps physical file data into logical scan data.
172    transform: kernel::ExpressionRef,
173}
174
175/// Storage options for one Delta source.
176pub type DeltaStorageOptions = BTreeMap<String, String>;
177
178/// Caller-provided configuration for one named Delta source.
179pub struct DeltaSourceConfig {
180    /// DataFusion table name that will identify this source.
181    pub name: String,
182    /// Caller-provided Delta table location.
183    pub table_uri: String,
184    /// Optional fixed Delta table version.
185    pub version: Option<Version>,
186    /// Source-local options forwarded to Delta Kernel object-store construction.
187    pub storage_options: DeltaStorageOptions,
188}
189
190impl DeltaSourceConfig {
191    /// Builds a Delta source config with no fixed version or storage options.
192    #[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    /// Sets an optional fixed Delta table version.
203    #[must_use]
204    pub fn with_version(mut self, version: Option<Version>) -> Self {
205        self.version = version;
206        self
207    }
208
209    /// Sets source-local storage options.
210    #[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
217/// Loaded named Delta source state.
218pub struct PlannedDeltaSource {
219    name: String,
220    requested_table_uri: String,
221    storage_options: DeltaStorageOptions,
222    snapshot: LoadedDeltaTableSnapshot,
223}
224
225/// Kernel-backed scan state for one projected Delta scan.
226#[allow(dead_code)]
227pub(crate) struct ProjectedDeltaScan {
228    scan: kernel::Scan,
229    kernel_schema: kernel::KernelSchemaRef,
230}
231
232impl ProjectedDeltaScan {
233    /// Returns the projected kernel schema selected for this scan.
234    #[allow(dead_code)]
235    #[must_use]
236    pub(crate) fn kernel_schema(&self) -> &kernel::KernelSchemaRef {
237        &self.kernel_schema
238    }
239
240    /// Returns the kernel scan handle that metadata planning consumes.
241    #[allow(dead_code)]
242    #[must_use]
243    pub(crate) fn kernel_scan(&self) -> &kernel::Scan {
244        &self.scan
245    }
246
247    /// Returns the kernel scan schema state needed by data-file reads.
248    #[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    /// Expands this kernel scan into metadata-only scan file records.
259    ///
260    /// This exhausts kernel scan metadata for one provider scan and does not
261    /// read Parquet data, load deletion-vector payloads, or apply physical data
262    /// transforms.
263    #[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    /// Returns scan file paths after kernel scan planning.
295    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    /// DataFusion table name for this source.
387    #[must_use]
388    pub fn name(&self) -> &str {
389        &self.name
390    }
391
392    /// Caller-provided Delta table location.
393    #[must_use]
394    pub fn requested_table_uri(&self) -> &str {
395        &self.requested_table_uri
396    }
397
398    /// Effective source-local options forwarded to object-store construction.
399    #[must_use]
400    pub fn storage_options(&self) -> &DeltaStorageOptions {
401        &self.storage_options
402    }
403
404    /// Normalized Delta table URI used for snapshot loading.
405    #[must_use]
406    pub fn table_uri(&self) -> &str {
407        self.loaded_snapshot().table_uri()
408    }
409
410    /// Loaded Delta table version.
411    #[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/// Builds kernel-backed scan state for a loaded Delta source projection.
436#[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/// Builds kernel-backed scan state for a loaded Delta source projection and predicate.
445///
446/// `projected_column_names` is the schema passed to delta_kernel's scan
447/// builder, not necessarily the final DataFusion output schema. Callers may
448/// include hidden predicate columns here when the final projection omits them.
449#[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/// Builds kernel-backed scan state with parsed file stats exposed in scan metadata.
468#[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
486/// Loads one named Delta source.
487///
488/// Name validation runs before URI normalization, engine construction, or
489/// snapshot loading.
490///
491/// # Errors
492///
493/// Returns [`DeltaFunnelError::InvalidSourceName`] for an invalid source name,
494/// [`DeltaFunnelError::InvalidSourceUri`] for an invalid table URI,
495/// [`DeltaFunnelError::DeltaSourceEngine`] when engine construction fails, or
496/// [`DeltaFunnelError::DeltaSnapshotLoad`] when the requested snapshot cannot
497/// be loaded.
498pub 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
514/// Loads configured Delta sources after validating all names.
515///
516/// Name validation and duplicate detection run before any URI normalization,
517/// engine construction, or snapshot loading.
518///
519/// # Errors
520///
521/// Returns [`DeltaFunnelError::InvalidSourceName`] or
522/// [`DeltaFunnelError::DuplicateSourceName`] before loading any snapshot when
523/// source names are invalid or ambiguous. Otherwise returns the same URI,
524/// engine, and snapshot-loading errors as [`load_delta_source`].
525pub 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                &timestamp_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                &timestamp_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}