From 350d5b26cfa24b3e8f27355259724ebe7836f100 Mon Sep 17 00:00:00 2001 From: Zach Schuermann Date: Mon, 13 Jan 2025 11:42:11 -0800 Subject: [PATCH 1/9] fix: release script publishing fixes (#638) release script publishing fixes --- release.sh | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/release.sh b/release.sh index 640fb4faa..b16605c9d 100755 --- a/release.sh +++ b/release.sh @@ -53,7 +53,7 @@ is_working_tree_clean() { is_version_published() { local crate_name="$1" local version - version=get_current_version "$crate_name" + version=$(get_current_version "$crate_name") if [[ -z "$version" ]]; then log_error "Could not find crate '$crate_name' in workspace" @@ -122,14 +122,20 @@ handle_release_branch() { # Handle main branch workflow (publish and tag) handle_main_branch() { # could potentially just use full 'cargo release' command here - publish "delta_kernel_derive" "$current_version" - publish "delta_kernel" "$current_version" + publish "delta_kernel_derive" + publish "delta_kernel" + + # hack: just redo getting the version + local version + version=$(get_current_version "delta_kernel") if confirm "Would you like to tag this release?"; then - log_info "Tagging release $current_version..." - git tag -a "v$current_version" -m "Release $current_version" - git push upstream "v$current_version" - log_success "Tagged release $current_version" + log_info "Tagging release $version..." + if confirm "Tagging as v$version. continue?"; then + git tag -a "v$version" -m "Release v$version" + git push upstream tag "v$version" + log_success "Tagged release $version" + fi fi } @@ -138,20 +144,20 @@ publish() { local current_version current_version=$(get_current_version "$crate_name") - if is_version_published "delta_kernel_derive"; then - log_error "delta_kernel_derive version $current_version is already published to crates.io" + if is_version_published "$crate_name"; then + log_error "$crate_name version $current_version is already published to crates.io" fi - log_info "[DRY RUN] Publishing $crate_name version $version to crates.io..." + log_info "[DRY RUN] Publishing $crate_name version $current_version to crates.io..." if ! cargo publish --dry-run -p "$crate_name"; then log_error "Failed to publish $crate_name to crates.io" fi if confirm "Dry run complete. Continue with publishing?"; then - log_info "Publishing $crate_name version $version to crates.io..." + log_info "Publishing $crate_name version $current_version to crates.io..." if ! cargo publish -p "$crate_name"; then log_error "Failed to publish $crate_name to crates.io" fi - log_success "Successfully published $crate_name version $version to crates.io" + log_success "Successfully published $crate_name version $current_version to crates.io" fi } From 12020d81a510433a1e78241b7c77c99529c77b32 Mon Sep 17 00:00:00 2001 From: Robert Pack <42610831+roeap@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:42:21 +0100 Subject: [PATCH 2/9] chore: fix some typos (#643) ## What changes are proposed in this pull request? Recently installed a spell checker in my IDE and thought I'd fix the findings ... ## How was this change tested? current tests, no changes to code / logic. Signed-off-by: Robert Pack --- .gitignore | 1 + README.md | 10 +++++----- ffi/examples/read-table/arrow.c | 4 ++-- ffi/examples/read-table/read_table.c | 2 +- ffi/src/engine_funcs.rs | 2 +- ffi/src/expressions/kernel.rs | 2 +- ffi/src/handle.rs | 4 ++-- ffi/src/scan.rs | 2 +- ffi/src/test_ffi.rs | 2 +- integration-tests/src/main.rs | 4 ++-- kernel/examples/inspect-table/src/main.rs | 2 +- kernel/examples/read-table-multi-threaded/src/main.rs | 6 +++--- kernel/examples/read-table-single-threaded/src/main.rs | 2 +- kernel/src/actions/visitors.rs | 2 +- kernel/src/engine/arrow_utils.rs | 6 +++--- kernel/src/engine/default/json.rs | 2 +- kernel/src/engine/default/parquet.rs | 8 ++++---- kernel/src/engine/sync/parquet.rs | 5 ++--- kernel/src/engine_data.rs | 2 +- kernel/src/error.rs | 6 +++--- kernel/src/expressions/mod.rs | 2 +- kernel/src/expressions/scalars.rs | 2 +- kernel/src/lib.rs | 4 ++-- kernel/src/predicates/parquet_stats_skipping/tests.rs | 4 ++-- kernel/src/predicates/tests.rs | 2 +- kernel/src/scan/data_skipping.rs | 2 +- kernel/src/schema.rs | 2 +- kernel/src/transaction.rs | 2 +- kernel/tests/golden_tables.rs | 8 ++++---- 29 files changed, 51 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index a7faecad9..11bf875df 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .idea/ .vscode/ .vim +.zed # Rust .cargo/ diff --git a/README.md b/README.md index 46ec1b10f..6e25a2ddb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Delta-kernel-rs is split into a few different crates: - kernel: The actual core kernel crate - acceptance: Acceptance tests that validate correctness via the [Delta Acceptance Tests][dat] - derive-macros: A crate for our [derive-macros] to live in -- ffi: Functionallity that enables delta-kernel-rs to be used from `C` or `C++` See the [ffi](ffi) +- ffi: Functionality that enables delta-kernel-rs to be used from `C` or `C++` See the [ffi](ffi) directory for more information. ## Building @@ -66,12 +66,12 @@ are still unstable. We therefore may break APIs within minor releases (that is, we will not break APIs in patch releases (`0.1.0` -> `0.1.1`). ## Arrow versioning -If you enable the `default-engine` or `sync-engine` features, you get an implemenation of the +If you enable the `default-engine` or `sync-engine` features, you get an implementation of the `Engine` trait that uses [Arrow] as its data format. The [`arrow crate`](https://docs.rs/arrow/latest/arrow/) tends to release new major versions rather quickly. To enable engines that already integrate arrow to also integrate kernel and not force them -to track a specific version of arrow that kernel depends on, we take as broad dependecy on arrow +to track a specific version of arrow that kernel depends on, we take as broad dependency on arrow versions as we can. This means you can force kernel to rely on the specific arrow version that your engine already uses, @@ -96,7 +96,7 @@ arrow-schema = "53.0" parquet = "53.0" ``` -Note that unfortunatly patching in `cargo` requires that _exactly one_ version matches your +Note that unfortunately patching in `cargo` requires that _exactly one_ version matches your specification. If only arrow "53.0.0" had been released the above will work, but if "53.0.1" where to be released, the specification will break and you will need to provide a more restrictive specification like `"=53.0.0"`. @@ -111,7 +111,7 @@ and then checking what version of `object_store` it depends on. ## Documentation - [API Docs](https://docs.rs/delta_kernel/latest/delta_kernel/) -- [arcitecture.md](doc/architecture.md) document describing the kernel architecture (currently wip) +- [architecture.md](doc/architecture.md) document describing the kernel architecture (currently wip) ## Examples diff --git a/ffi/examples/read-table/arrow.c b/ffi/examples/read-table/arrow.c index d58a2fa2d..7eb32b7c3 100644 --- a/ffi/examples/read-table/arrow.c +++ b/ffi/examples/read-table/arrow.c @@ -97,7 +97,7 @@ static GArrowRecordBatch* add_partition_columns( } GArrowArray* partition_col = garrow_array_builder_finish((GArrowArrayBuilder*)builder, &error); - if (report_g_error("Can't build string array for parition column", error)) { + if (report_g_error("Can't build string array for partition column", error)) { printf("Giving up on column %s\n", col); g_error_free(error); g_object_unref(builder); @@ -144,7 +144,7 @@ static void add_batch_to_context( } record_batch = add_partition_columns(record_batch, partition_cols, partition_values); if (record_batch == NULL) { - printf("Failed to add parition columns, not adding batch\n"); + printf("Failed to add partition columns, not adding batch\n"); return; } context->batches = g_list_append(context->batches, record_batch); diff --git a/ffi/examples/read-table/read_table.c b/ffi/examples/read-table/read_table.c index 0aa8caa41..7b1a7f2c7 100644 --- a/ffi/examples/read-table/read_table.c +++ b/ffi/examples/read-table/read_table.c @@ -43,7 +43,7 @@ void print_partition_info(struct EngineContext* context, const CStringMap* parti } // Kernel will call this function for each file that should be scanned. The arguments include enough -// context to constuct the correct logical data from the physically read parquet +// context to construct the correct logical data from the physically read parquet void scan_row_callback( void* engine_context, KernelStringSlice path, diff --git a/ffi/src/engine_funcs.rs b/ffi/src/engine_funcs.rs index f8534dfc0..1afb60510 100644 --- a/ffi/src/engine_funcs.rs +++ b/ffi/src/engine_funcs.rs @@ -42,7 +42,7 @@ impl Drop for FileReadResultIterator { } } -/// Call the engine back with the next `EngingeData` batch read by Parquet/Json handler. The +/// Call the engine back with the next `EngineData` batch read by Parquet/Json handler. The /// _engine_ "owns" the data that is passed into the `engine_visitor`, since it is allocated by the /// `Engine` being used for log-replay. If the engine wants the kernel to free this data, it _must_ /// call [`free_engine_data`] on it. diff --git a/ffi/src/expressions/kernel.rs b/ffi/src/expressions/kernel.rs index f2ed8b1a3..a5116db47 100644 --- a/ffi/src/expressions/kernel.rs +++ b/ffi/src/expressions/kernel.rs @@ -83,7 +83,7 @@ pub struct EngineExpressionVisitor { /// Visit a 64bit timestamp belonging to the list identified by `sibling_list_id`. /// The timestamp is microsecond precision with no timezone. pub visit_literal_timestamp_ntz: VisitLiteralFn, - /// Visit a 32bit intger `date` representing days since UNIX epoch 1970-01-01. The `date` belongs + /// Visit a 32bit integer `date` representing days since UNIX epoch 1970-01-01. The `date` belongs /// to the list identified by `sibling_list_id`. pub visit_literal_date: VisitLiteralFn, /// Visit binary data at the `buffer` with length `len` belonging to the list identified by diff --git a/ffi/src/handle.rs b/ffi/src/handle.rs index 27b35bea5..30b695ecc 100644 --- a/ffi/src/handle.rs +++ b/ffi/src/handle.rs @@ -2,8 +2,8 @@ //! boundary. //! //! Creating a [`Handle`] always implies some kind of ownership transfer. A mutable handle takes -//! ownership of the object itself (analagous to [`Box`]), while a non-mutable (shared) handle -//! takes ownership of a shared reference to the object (analagous to [`std::sync::Arc`]). Thus, a created +//! ownership of the object itself (analogous to [`Box`]), while a non-mutable (shared) handle +//! takes ownership of a shared reference to the object (analogous to [`std::sync::Arc`]). Thus, a created //! handle remains [valid][Handle#Validity], and its underlying object remains accessible, until the //! handle is explicitly dropped or consumed. Dropping a mutable handle always drops the underlying //! object as well; dropping a shared handle only drops the underlying object if the handle was the diff --git a/ffi/src/scan.rs b/ffi/src/scan.rs index d5695c130..86f5e7e5f 100644 --- a/ffi/src/scan.rs +++ b/ffi/src/scan.rs @@ -383,7 +383,7 @@ struct ContextWrapper { /// data which provides the data handle and selection vector as each element in the iterator. /// /// # Safety -/// engine is responsbile for passing a valid [`ExclusiveEngineData`] and selection vector. +/// engine is responsible for passing a valid [`ExclusiveEngineData`] and selection vector. #[no_mangle] pub unsafe extern "C" fn visit_scan_data( data: Handle, diff --git a/ffi/src/test_ffi.rs b/ffi/src/test_ffi.rs index 27c7063fa..14eec1b86 100644 --- a/ffi/src/test_ffi.rs +++ b/ffi/src/test_ffi.rs @@ -12,7 +12,7 @@ use delta_kernel::{ /// output expression can be found in `ffi/tests/test_expression_visitor/expected.txt`. /// /// # Safety -/// The caller is responsible for freeing the retured memory, either by calling +/// The caller is responsible for freeing the returned memory, either by calling /// [`free_kernel_predicate`], or [`Handle::drop_handle`] #[no_mangle] pub unsafe extern "C" fn get_testing_kernel_expression() -> Handle { diff --git a/integration-tests/src/main.rs b/integration-tests/src/main.rs index 63adb3940..a5bfe0952 100644 --- a/integration-tests/src/main.rs +++ b/integration-tests/src/main.rs @@ -15,8 +15,8 @@ fn create_kernel_schema() -> delta_kernel::schema::Schema { fn main() { let arrow_schema = create_arrow_schema(); let kernel_schema = create_kernel_schema(); - let convereted: delta_kernel::schema::Schema = + let converted: delta_kernel::schema::Schema = delta_kernel::schema::Schema::try_from(&arrow_schema).expect("couldn't convert"); - assert!(kernel_schema == convereted); + assert!(kernel_schema == converted); println!("Okay, made it"); } diff --git a/kernel/examples/inspect-table/src/main.rs b/kernel/examples/inspect-table/src/main.rs index ea25a8404..194530004 100644 --- a/kernel/examples/inspect-table/src/main.rs +++ b/kernel/examples/inspect-table/src/main.rs @@ -184,7 +184,7 @@ fn print_scan_file( fn try_main() -> DeltaResult<()> { let cli = Cli::parse(); - // build a table and get the lastest snapshot from it + // build a table and get the latest snapshot from it let table = Table::try_from_uri(&cli.path)?; let engine = DefaultEngine::try_new( diff --git a/kernel/examples/read-table-multi-threaded/src/main.rs b/kernel/examples/read-table-multi-threaded/src/main.rs index d97b6c2d3..e689a4ef4 100644 --- a/kernel/examples/read-table-multi-threaded/src/main.rs +++ b/kernel/examples/read-table-multi-threaded/src/main.rs @@ -104,7 +104,7 @@ fn truncate_batch(batch: RecordBatch, rows: usize) -> RecordBatch { RecordBatch::try_new(batch.schema(), cols).unwrap() } -// This is the callback that will be called fo each valid scan row +// This is the callback that will be called for each valid scan row fn send_scan_file( scan_tx: &mut spmc::Sender, path: &str, @@ -125,7 +125,7 @@ fn send_scan_file( fn try_main() -> DeltaResult<()> { let cli = Cli::parse(); - // build a table and get the lastest snapshot from it + // build a table and get the latest snapshot from it let table = Table::try_from_uri(&cli.path)?; println!("Reading {}", table.location()); @@ -279,7 +279,7 @@ fn do_work( // this example uses the parquet_handler from the engine, but an engine could // choose to use whatever method it might want to read a parquet file. The reader - // could, for example, fill in the parition columns, or apply deletion vectors. Here + // could, for example, fill in the partition columns, or apply deletion vectors. Here // we assume a more naive parquet reader and fix the data up after the fact. // further parallelism would also be possible here as we could read the parquet file // in chunks where each thread reads one chunk. The engine would need to ensure diff --git a/kernel/examples/read-table-single-threaded/src/main.rs b/kernel/examples/read-table-single-threaded/src/main.rs index 32ad3173d..9bbc9476d 100644 --- a/kernel/examples/read-table-single-threaded/src/main.rs +++ b/kernel/examples/read-table-single-threaded/src/main.rs @@ -69,7 +69,7 @@ fn main() -> ExitCode { fn try_main() -> DeltaResult<()> { let cli = Cli::parse(); - // build a table and get the lastest snapshot from it + // build a table and get the latest snapshot from it let table = Table::try_from_uri(&cli.path)?; println!("Reading {}", table.location()); diff --git a/kernel/src/actions/visitors.rs b/kernel/src/actions/visitors.rs index 7d4be1a82..0cd12ce50 100644 --- a/kernel/src/actions/visitors.rs +++ b/kernel/src/actions/visitors.rs @@ -367,7 +367,7 @@ impl RowVisitor for CdcVisitor { pub type SetTransactionMap = HashMap; -/// Extact application transaction actions from the log into a map +/// Extract application transaction actions from the log into a map /// /// This visitor maintains the first entry for each application id it /// encounters. When a specific application id is required then diff --git a/kernel/src/engine/arrow_utils.rs b/kernel/src/engine/arrow_utils.rs index 4700b72c0..a3e184574 100644 --- a/kernel/src/engine/arrow_utils.rs +++ b/kernel/src/engine/arrow_utils.rs @@ -55,9 +55,9 @@ macro_rules! prim_array_cmp { pub(crate) use prim_array_cmp; -/// Get the indicies in `parquet_schema` of the specified columns in `requested_schema`. This -/// returns a tuples of (mask_indicies: Vec, reorder_indicies: -/// Vec). `mask_indicies` is used for generating the mask for reading from the +/// Get the indices in `parquet_schema` of the specified columns in `requested_schema`. This +/// returns a tuples of (mask_indices: Vec, reorder_indices: +/// Vec). `mask_indices` is used for generating the mask for reading from the pub(crate) fn make_arrow_error(s: impl Into) -> Error { Error::Arrow(arrow_schema::ArrowError::InvalidArgumentError(s.into())).with_backtrace() } diff --git a/kernel/src/engine/default/json.rs b/kernel/src/engine/default/json.rs index 1912a7b34..ab296e12a 100644 --- a/kernel/src/engine/default/json.rs +++ b/kernel/src/engine/default/json.rs @@ -29,7 +29,7 @@ pub struct DefaultJsonHandler { store: Arc, /// The executor to run async tasks on task_executor: Arc, - /// The maximun number of batches to read ahead + /// The maximum number of batches to read ahead readahead: usize, /// The number of rows to read per batch batch_size: usize, diff --git a/kernel/src/engine/default/parquet.rs b/kernel/src/engine/default/parquet.rs index 1acc4ef4a..a65d329a2 100644 --- a/kernel/src/engine/default/parquet.rs +++ b/kernel/src/engine/default/parquet.rs @@ -258,7 +258,7 @@ impl FileOpener for ParquetOpener { let mut reader = ParquetObjectReader::new(store, meta); let metadata = ArrowReaderMetadata::load_async(&mut reader, Default::default()).await?; let parquet_schema = metadata.schema(); - let (indicies, requested_ordering) = + let (indices, requested_ordering) = get_requested_indices(&table_schema, parquet_schema)?; let options = ArrowReaderOptions::new(); //.with_page_index(enable_page_index); let mut builder = @@ -267,7 +267,7 @@ impl FileOpener for ParquetOpener { &table_schema, parquet_schema, builder.parquet_schema(), - &indicies, + &indices, ) { builder = builder.with_projection(mask) } @@ -330,7 +330,7 @@ impl FileOpener for PresignedUrlOpener { let reader = client.get(file_meta.location).send().await?.bytes().await?; let metadata = ArrowReaderMetadata::load(&reader, Default::default())?; let parquet_schema = metadata.schema(); - let (indicies, requested_ordering) = + let (indices, requested_ordering) = get_requested_indices(&table_schema, parquet_schema)?; let options = ArrowReaderOptions::new(); @@ -340,7 +340,7 @@ impl FileOpener for PresignedUrlOpener { &table_schema, parquet_schema, builder.parquet_schema(), - &indicies, + &indices, ) { builder = builder.with_projection(mask) } diff --git a/kernel/src/engine/sync/parquet.rs b/kernel/src/engine/sync/parquet.rs index 2a54e2e86..260ef321b 100644 --- a/kernel/src/engine/sync/parquet.rs +++ b/kernel/src/engine/sync/parquet.rs @@ -21,9 +21,8 @@ fn try_create_from_parquet( let metadata = ArrowReaderMetadata::load(&file, Default::default())?; let parquet_schema = metadata.schema(); let mut builder = ParquetRecordBatchReaderBuilder::try_new(file)?; - let (indicies, requested_ordering) = get_requested_indices(&schema, parquet_schema)?; - if let Some(mask) = generate_mask(&schema, parquet_schema, builder.parquet_schema(), &indicies) - { + let (indices, requested_ordering) = get_requested_indices(&schema, parquet_schema)?; + if let Some(mask) = generate_mask(&schema, parquet_schema, builder.parquet_schema(), &indices) { builder = builder.with_projection(mask); } if let Some(predicate) = predicate { diff --git a/kernel/src/engine_data.rs b/kernel/src/engine_data.rs index e421d0ad6..25a7e84bd 100644 --- a/kernel/src/engine_data.rs +++ b/kernel/src/engine_data.rs @@ -199,7 +199,7 @@ pub trait RowVisitor { /// "getter" of type [`GetData`] will be present. This can be used to actually get at the data /// for each row. You can `use` the `TypedGetData` trait if you want to have a way to extract /// typed data that will fail if the "getter" is for an unexpected type. The data in `getters` - /// does not outlive the call to this funtion (i.e. it should be copied if needed). + /// does not outlive the call to this function (i.e. it should be copied if needed). fn visit<'a>(&mut self, row_count: usize, getters: &[&'a dyn GetData<'a>]) -> DeltaResult<()>; /// Visit the rows of an [`EngineData`], selecting the leaf column names given by diff --git a/kernel/src/error.rs b/kernel/src/error.rs index e3230aeb9..815ef3e51 100644 --- a/kernel/src/error.rs +++ b/kernel/src/error.rs @@ -1,4 +1,4 @@ -//! Defintions of errors that the delta kernel can encounter +//! Definitions of errors that the delta kernel can encounter use std::{ backtrace::{Backtrace, BacktraceStatus}, @@ -58,7 +58,7 @@ pub enum Error { #[error("Internal error {0}. This is a kernel bug, please report.")] InternalError(String), - /// An error enountered while working with parquet data + /// An error encountered while working with parquet data #[cfg(feature = "parquet")] #[error("Arrow error: {0}")] Parquet(#[from] parquet::errors::ParquetError), @@ -99,7 +99,7 @@ pub enum Error { #[error("No table version found.")] MissingVersion, - /// An error occured while working with deletion vectors + /// An error occurred while working with deletion vectors #[error("Deletion Vector error: {0}")] DeletionVector(String), diff --git a/kernel/src/expressions/mod.rs b/kernel/src/expressions/mod.rs index bad20aea4..620142679 100644 --- a/kernel/src/expressions/mod.rs +++ b/kernel/src/expressions/mod.rs @@ -737,7 +737,7 @@ mod tests { ), ]); - // Similer to ExpressionDepthChecker::check, but also returns call count + // Similar to ExpressionDepthChecker::check, but also returns call count let check_with_call_count = |depth_limit| ExpressionDepthChecker::check_with_call_count(&expr, depth_limit); diff --git a/kernel/src/expressions/scalars.rs b/kernel/src/expressions/scalars.rs index 5283c08c5..2ce2fd41a 100644 --- a/kernel/src/expressions/scalars.rs +++ b/kernel/src/expressions/scalars.rs @@ -393,7 +393,7 @@ impl PrimitiveType { // Timestamps may additionally be encoded as a ISO 8601 formatted string such as // `1970-01-01T00:00:00.123456Z`. // - // The difference arrises mostly in how they are to be handled on the engine side - i.e. timestampNTZ + // The difference arises mostly in how they are to be handled on the engine side - i.e. timestampNTZ // is not adjusted to UTC, this is just so we can (de-)serialize it as a date sting. // https://github.com/delta-io/delta/blob/master/PROTOCOL.md#partition-value-serialization TimestampNtz | Timestamp => { diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index f27907bcd..fa88e7afa 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -202,7 +202,7 @@ impl FileMeta { /// let b: Arc = a.downcast().unwrap(); /// ``` /// -/// In contrast, very similer code that relies only on `Any` would fail to compile: +/// In contrast, very similar code that relies only on `Any` would fail to compile: /// /// ```fail_compile /// # use std::any::Any; @@ -404,7 +404,7 @@ pub trait JsonHandler: AsAny { /// /// - `path` - URL specifying the location to write the JSON file /// - `data` - Iterator of EngineData to write to the JSON file. Each row should be written as - /// a new JSON object appended to the file. (that is, the file is newline-delimeted JSON, and + /// a new JSON object appended to the file. (that is, the file is newline-delimited JSON, and /// each row is a JSON object on a single line) /// - `overwrite` - If true, overwrite the file if it exists. If false, the call must fail if /// the file exists. diff --git a/kernel/src/predicates/parquet_stats_skipping/tests.rs b/kernel/src/predicates/parquet_stats_skipping/tests.rs index b1de88e6b..50833a166 100644 --- a/kernel/src/predicates/parquet_stats_skipping/tests.rs +++ b/kernel/src/predicates/parquet_stats_skipping/tests.rs @@ -299,7 +299,7 @@ fn test_sql_where() { "WHERE {TRUE} < {FALSE}" ); - // Constrast normal vs SQL WHERE semantics - comparison + // Contrast normal vs SQL WHERE semantics - comparison expect_eq!( AllNullTestFilter.eval_expr(&Expr::lt(col.clone(), VAL), false), None, @@ -321,7 +321,7 @@ fn test_sql_where() { "WHERE {VAL} < {col}" ); - // Constrast normal vs SQL WHERE semantics - comparison inside AND + // Contrast normal vs SQL WHERE semantics - comparison inside AND expect_eq!( AllNullTestFilter.eval_expr(&Expr::and(NULL, Expr::lt(col.clone(), VAL)), false), None, diff --git a/kernel/src/predicates/tests.rs b/kernel/src/predicates/tests.rs index ce273e7b8..fa4aec191 100644 --- a/kernel/src/predicates/tests.rs +++ b/kernel/src/predicates/tests.rs @@ -51,7 +51,7 @@ fn test_default_eval_scalar() { } } -// verifies that partial orderings behave as excpected for all Scalar types +// verifies that partial orderings behave as expected for all Scalar types #[test] fn test_default_partial_cmp_scalars() { use Ordering::*; diff --git a/kernel/src/scan/data_skipping.rs b/kernel/src/scan/data_skipping.rs index 54eb5344c..847855d4a 100644 --- a/kernel/src/scan/data_skipping.rs +++ b/kernel/src/scan/data_skipping.rs @@ -24,7 +24,7 @@ mod tests; /// Returns `None` if the predicate is not eligible for data skipping. /// /// We normalize each binary operation to a comparison between a column and a literal value and -/// rewite that in terms of the min/max values of the column. +/// rewrite that in terms of the min/max values of the column. /// For example, `1 < a` is rewritten as `minValues.a > 1`. /// /// For Unary `Not`, we push the Not down using De Morgan's Laws to invert everything below the Not. diff --git a/kernel/src/schema.rs b/kernel/src/schema.rs index 42901751f..9cb6769f9 100644 --- a/kernel/src/schema.rs +++ b/kernel/src/schema.rs @@ -1156,7 +1156,7 @@ mod tests { ), ]); - // Similer to SchemaDepthChecker::check, but also returns call count + // Similar to SchemaDepthChecker::check, but also returns call count let check_with_call_count = |depth_limit| SchemaDepthChecker::check_with_call_count(&schema, depth_limit); diff --git a/kernel/src/transaction.rs b/kernel/src/transaction.rs index c6e93ea7b..c73782f64 100644 --- a/kernel/src/transaction.rs +++ b/kernel/src/transaction.rs @@ -241,7 +241,7 @@ impl WriteContext { /// Result after committing a transaction. If 'committed', the version is the new version written /// to the log. If 'conflict', the transaction is returned so the caller can resolve the conflict /// (along with the version which conflicted). -// TODO(zach): in order to make the returning of a transcation useful, we need to add APIs to +// TODO(zach): in order to make the returning of a transaction useful, we need to add APIs to // update the transaction to a new version etc. #[derive(Debug)] pub enum CommitResult { diff --git a/kernel/tests/golden_tables.rs b/kernel/tests/golden_tables.rs index cd9023db1..120271ef2 100644 --- a/kernel/tests/golden_tables.rs +++ b/kernel/tests/golden_tables.rs @@ -26,7 +26,7 @@ use delta_kernel::engine::default::DefaultEngine; mod common; use common::{load_test_data, to_arrow}; -// NB adapated from DAT: read all parquet files in the directory and concatenate them +// NB adapted from DAT: read all parquet files in the directory and concatenate them async fn read_expected(path: &Path) -> DeltaResult { let store = Arc::new(LocalFileSystem::new_with_prefix(path)?); let files = store.list(None).try_collect::>().await?; @@ -368,7 +368,7 @@ golden_test!("deltalog-getChanges", latest_snapshot_test); golden_test!("dv-partitioned-with-checkpoint", latest_snapshot_test); golden_test!("dv-with-columnmapping", latest_snapshot_test); -skip_test!("hive": "test not yet implmented - different file structure"); +skip_test!("hive": "test not yet implemented - different file structure"); golden_test!("kernel-timestamp-int96", latest_snapshot_test); golden_test!("kernel-timestamp-pst", latest_snapshot_test); golden_test!("kernel-timestamp-timestamp_micros", latest_snapshot_test); @@ -436,11 +436,11 @@ skip_test!("canonicalized-paths-special-b": "BUG: path canonicalization"); // // We added two add files with the same path `foo`. The first should have been removed. // // The second should remain, and should have a hard-coded modification time of 1700000000000L // assert(foundFiles.find(_.getPath.endsWith("foo")).exists(_.getModificationTime == 1700000000000L)) -skip_test!("delete-re-add-same-file-different-transactions": "test not yet implmented"); +skip_test!("delete-re-add-same-file-different-transactions": "test not yet implemented"); // data file doesn't exist, get the relative path to compare // assert(new File(addFileStatus.getPath).getName == "special p@#h") -skip_test!("log-replay-special-characters-b": "test not yet implmented"); +skip_test!("log-replay-special-characters-b": "test not yet implemented"); negative_test!("deltalog-invalid-protocol-version"); negative_test!("deltalog-state-reconstruction-from-checkpoint-missing-metadata"); From b3546f0ef1d2d9639eedf89d04570b74014a8144 Mon Sep 17 00:00:00 2001 From: Oussama Saoudi Date: Tue, 14 Jan 2025 12:03:03 -0800 Subject: [PATCH 3/9] docs: Update readme to reflect tracing feature is needed for read-table (#619) ## What changes are proposed in this pull request? Updates the docs to reflect that tracing is required to be enabled in the `read_table.c` example code. ## How was this change tested? Compiling without the tracing feature yields the following error: ``` Undefined symbols for architecture arm64: "_enable_event_tracing", referenced from: _main in read_table.c.o ``` And compiling with it works. --- ffi/README.md | 2 +- ffi/examples/read-table/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 6106b685f..47ea31e60 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -8,7 +8,7 @@ This crate provides a c foreign function internface (ffi) for delta-kernel-rs. You can build static and shared-libraries, as well as the include headers by simply running: ```sh -cargo build [--release] [--features default-engine] +cargo build [--release] ``` This will place libraries in the root `target` dir (`../target/[debug,release]` from the directory containing this README), and headers in `../target/ffi-headers`. In that directory there will be a `delta_kernel_ffi.h` file, which is the C header, and a `delta_kernel_ffi.hpp` which is the C++ header. diff --git a/ffi/examples/read-table/README.md b/ffi/examples/read-table/README.md index 4debb048e..e70d1e42a 100644 --- a/ffi/examples/read-table/README.md +++ b/ffi/examples/read-table/README.md @@ -10,9 +10,9 @@ This example is built with [cmake]. Instructions below assume you start in the d Note that prior to building these examples you must build `delta_kernel_ffi` (see [the FFI readme] for details). TLDR: ```bash # from repo root -$ cargo build -p delta_kernel_ffi [--release] [--features default-engine, tracing] +$ cargo build -p delta_kernel_ffi [--release] --features tracing # from ffi/ dir -$ cargo build [--release] [--features default-engine, tracing] +$ cargo build [--release] --features tracing ``` There are two configurations that can currently be configured in cmake: From 9c43bf432084006ef090ef1f058f3eb0888a4806 Mon Sep 17 00:00:00 2001 From: OussamaSaoudi <45303303+OussamaSaoudi@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:22:22 -0800 Subject: [PATCH 4/9] doc: Clarify `JsonHandler` semantics on EngineData ordering (#635) ## What changes are proposed in this pull request? When reading multiple log files during log replay, it is important that we read commits in order. This ensures the correctness of add/remove deduplication. Hence, we are implicitly relying on the commit files being read in order by the json handler. Moreover when in-commit timestamps is enabled, the ordering of batches of engine data in a commit is important. A correct delta table should have the commit info be the _first_ action in a log file. --- kernel/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index fa88e7afa..49dceea75 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -371,8 +371,20 @@ pub trait JsonHandler: AsAny { output_schema: SchemaRef, ) -> DeltaResult>; - /// Read and parse the JSON format file at given locations and return - /// the data as EngineData with the columns requested by physical schema. + /// Read and parse the JSON format file at given locations and return the data as EngineData with + /// the columns requested by physical schema. Note: The [`FileDataReadResultIterator`] must emit + /// data from files in the order that `files` is given. For example if files ["a", "b"] is provided, + /// then the engine data iterator must first return all the engine data from file "a", _then_ all + /// the engine data from file "b". Moreover, for a given file, all of its [`EngineData`] and + /// constituent rows must be in order that they occur in the file. Consider a file with rows + /// (1, 2, 3). The following are legal iterator batches: + /// iter: [EngineData(1, 2), EngineData(3)] + /// iter: [EngineData(1), EngineData(2, 3)] + /// iter: [EngineData(1, 2, 3)] + /// The following are illegal batches: + /// iter: [EngineData(3), EngineData(1, 2)] + /// iter: [EngineData(1), EngineData(3, 2)] + /// iter: [EngineData(2, 1, 3)] /// /// # Parameters /// From c1c1dbe46a661cb3919ac8b1b9e71177293a6bc7 Mon Sep 17 00:00:00 2001 From: Sebastian Tia <75666019+sebastiantia@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:35:50 -0800 Subject: [PATCH 5/9] feat: read `partition_values` in `RemoveVisitor` and remove `break` in `RowVisitor` for `RemoveVisitor` (#633) ## What changes are proposed in this pull request? 1. This PR updates `RemoveVisitor` to read `partition_values` 2. This PR removes the stray break in the implementation of `RowVisitor` for `RemoveVisitor` which prevented reading multiple `Remove` actions in a commit ## How was this change tested? - Introduced test to parse the `partition_values` field from `Remove` actions --- kernel/src/actions/mod.rs | 2 +- kernel/src/actions/visitors.rs | 54 ++++++++++++++++---- kernel/src/schema.rs | 2 +- kernel/src/table_changes/log_replay/tests.rs | 1 + 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/kernel/src/actions/mod.rs b/kernel/src/actions/mod.rs index c62486873..630eedc81 100644 --- a/kernel/src/actions/mod.rs +++ b/kernel/src/actions/mod.rs @@ -375,7 +375,7 @@ pub struct Add { /// in the added file must be contained in one or more remove actions in the same version. pub data_change: bool, - /// Contains [statistics] (e.g., count, min/max values for columns) about the data in this logical file. + /// Contains [statistics] (e.g., count, min/max values for columns) about the data in this logical file encoded as a JSON string. /// /// [statistics]: https://github.com/delta-io/delta/blob/master/PROTOCOL.md#Per-file-Statistics #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] diff --git a/kernel/src/actions/visitors.rs b/kernel/src/actions/visitors.rs index 0cd12ce50..957befe80 100644 --- a/kernel/src/actions/visitors.rs +++ b/kernel/src/actions/visitors.rs @@ -267,7 +267,8 @@ impl RemoveVisitor { let extended_file_metadata: Option = getters[3].get_opt(row_index, "remove.extendedFileMetadata")?; - // TODO(nick) handle partition values in getters[4] + let partition_values: Option> = + getters[4].get_opt(row_index, "remove.partitionValues")?; let size: Option = getters[5].get_opt(row_index, "remove.size")?; @@ -284,7 +285,7 @@ impl RemoveVisitor { data_change, deletion_timestamp, extended_file_metadata, - partition_values: None, + partition_values, size, tags: None, deletion_vector, @@ -305,10 +306,9 @@ impl RowVisitor for RemoveVisitor { } fn visit<'a>(&mut self, row_count: usize, getters: &[&'a dyn GetData<'a>]) -> DeltaResult<()> { for i in 0..row_count { - // Since path column is required, use it to detect presence of an Remove action + // Since path column is required, use it to detect presence of a Remove action if let Some(path) = getters[0].get_opt(i, "remove.path")? { self.removes.push(Self::visit_remove(i, path, getters)?); - break; } } Ok(()) @@ -603,11 +603,7 @@ mod tests { modification_time: 1670892998135, data_change: true, stats: Some("{\"numRecords\":1,\"minValues\":{\"c3\":5},\"maxValues\":{\"c3\":5},\"nullCount\":{\"c3\":0}}".into()), - tags: None, - deletion_vector: None, - base_row_id: None, - default_row_commit_version: None, - clustering_provider: None, + ..Default::default() }; let add2 = Add { path: "c1=5/c2=b/part-00007-4e73fa3b-2c88-424a-8051-f8b54328ffdb.c000.snappy.parquet".into(), @@ -630,11 +626,51 @@ mod tests { ..add1.clone() }; let expected = vec![add1, add2, add3]; + assert_eq!(add_visitor.adds.len(), expected.len()); for (add, expected) in add_visitor.adds.into_iter().zip(expected.into_iter()) { assert_eq!(add, expected); } } + #[test] + fn test_parse_remove_partitioned() { + let engine = SyncEngine::new(); + let json_handler = engine.get_json_handler(); + let json_strings: StringArray = vec![ + r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}"#, + r#"{"metaData":{"id":"aff5cb91-8cd9-4195-aef9-446908507302","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"c1\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},{\"name\":\"c2\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},{\"name\":\"c3\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":["c1","c2"],"configuration":{},"createdTime":1670892997849}}"#, + r#"{"remove":{"path":"c1=4/c2=c/part-00003-f525f459-34f9-46f5-82d6-d42121d883fd.c000.snappy.parquet","deletionTimestamp":1670892998135,"dataChange":true,"partitionValues":{"c1":"4","c2":"c"},"size":452}}"#, + ] + .into(); + let output_schema = get_log_schema().clone(); + let batch = json_handler + .parse_json(string_array_to_engine_data(json_strings), output_schema) + .unwrap(); + let mut remove_visitor = RemoveVisitor::default(); + remove_visitor.visit_rows_of(batch.as_ref()).unwrap(); + let expected_remove = Remove { + path: "c1=4/c2=c/part-00003-f525f459-34f9-46f5-82d6-d42121d883fd.c000.snappy.parquet" + .into(), + deletion_timestamp: Some(1670892998135), + data_change: true, + partition_values: Some(HashMap::from([ + ("c1".to_string(), "4".to_string()), + ("c2".to_string(), "c".to_string()), + ])), + size: Some(452), + ..Default::default() + }; + assert_eq!( + remove_visitor.removes.len(), + 1, + "Unexpected number of remove actions" + ); + assert_eq!( + remove_visitor.removes[0], expected_remove, + "Unexpected remove action" + ); + } + #[test] fn test_parse_txn() { let engine = SyncEngine::new(); diff --git a/kernel/src/schema.rs b/kernel/src/schema.rs index 9cb6769f9..d2ff65193 100644 --- a/kernel/src/schema.rs +++ b/kernel/src/schema.rs @@ -223,7 +223,7 @@ pub struct StructType { pub type_name: String, /// The type of element stored in this array // We use indexmap to preserve the order of fields as they are defined in the schema - // while also allowing for fast lookup by name. The atlerative to do a liner search + // while also allowing for fast lookup by name. The alternative is to do a linear search // for each field by name would be potentially quite expensive for large schemas. pub fields: IndexMap, } diff --git a/kernel/src/table_changes/log_replay/tests.rs b/kernel/src/table_changes/log_replay/tests.rs index f2dbdd956..29e076c07 100644 --- a/kernel/src/table_changes/log_replay/tests.rs +++ b/kernel/src/table_changes/log_replay/tests.rs @@ -469,6 +469,7 @@ async fn dv() { assert_eq!(sv, &[false, true, true]); } +// Note: Data skipping does not work on Remove actions. #[tokio::test] async fn data_skipping_filter() { let engine = Arc::new(SyncEngine::new()); From 606db20a68eb4d3c090b9f1e6d6fcda05bb5768a Mon Sep 17 00:00:00 2001 From: Oussama Saoudi Date: Tue, 14 Jan 2025 16:12:38 -0800 Subject: [PATCH 6/9] test: Port cdf tests from delta-spark to kernel (#611) ## What changes are proposed in this pull request? This PR adds several CDF tests from delta-spark. We check the following: - CDF over various version ranges - Update operations are read correctly from cdc files - data_change=false means the action is skipped - A range with start > end is an error. - Start version greater than latest table version is an error - CDF works on partition tables - CDF works on tables with backticks in the column names - CDF is correct in deletion cases with unconditional deletes, conditional deletes that remove all rows, and selective conditional deletes. Table-changes construction is also changed so that CDF version error is checked before snapshots are created. This makes the error message clearer in the case that the start version is beyond the end of the table. --- kernel/src/table_changes/mod.rs | 16 +- kernel/tests/cdf.rs | 321 +++++++++++++++++- .../cdf-table-backtick-column-names.tar.zst | Bin 0 -> 5690 bytes .../tests/data/cdf-table-data-change.tar.zst | Bin 0 -> 4850 bytes ...-table-delete-conditional-all-rows.tar.zst | Bin 0 -> 6188 bytes ...-table-delete-conditional-two-rows.tar.zst | Bin 0 -> 6772 bytes .../cdf-table-delete-unconditional.tar.zst | Bin 0 -> 4785 bytes .../data/cdf-table-non-partitioned.tar.zst | Bin 11152 -> 20033 bytes .../tests/data/cdf-table-partitioned.tar.zst | Bin 0 -> 8361 bytes kernel/tests/data/cdf-table-simple.tar.zst | Bin 0 -> 5889 bytes .../tests/data/cdf-table-update-ops.tar.zst | Bin 0 -> 6282 bytes .../data/cdf-table-with-cdc-and-dvs.tar.zst | Bin 19759 -> 37166 bytes kernel/tests/data/cdf-table-with-dv.tar.zst | Bin 2310 -> 4112 bytes kernel/tests/data/cdf-table.tar.zst | Bin 9540 -> 16311 bytes 14 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 kernel/tests/data/cdf-table-backtick-column-names.tar.zst create mode 100644 kernel/tests/data/cdf-table-data-change.tar.zst create mode 100644 kernel/tests/data/cdf-table-delete-conditional-all-rows.tar.zst create mode 100644 kernel/tests/data/cdf-table-delete-conditional-two-rows.tar.zst create mode 100644 kernel/tests/data/cdf-table-delete-unconditional.tar.zst create mode 100644 kernel/tests/data/cdf-table-partitioned.tar.zst create mode 100644 kernel/tests/data/cdf-table-simple.tar.zst create mode 100644 kernel/tests/data/cdf-table-update-ops.tar.zst diff --git a/kernel/src/table_changes/mod.rs b/kernel/src/table_changes/mod.rs index b74f65b7a..dad2f4e9b 100644 --- a/kernel/src/table_changes/mod.rs +++ b/kernel/src/table_changes/mod.rs @@ -138,6 +138,14 @@ impl TableChanges { start_version: Version, end_version: Option, ) -> DeltaResult { + let log_root = table_root.join("_delta_log/")?; + let log_segment = LogSegment::for_table_changes( + engine.get_file_system_client().as_ref(), + log_root, + start_version, + end_version, + )?; + // Both snapshots ensure that reading is supported at the start and end version using // `ensure_read_supported`. Note that we must still verify that reading is // supported for every protocol action in the CDF range. @@ -173,14 +181,6 @@ impl TableChanges { ))); } - let log_root = table_root.join("_delta_log/")?; - let log_segment = LogSegment::for_table_changes( - engine.get_file_system_client().as_ref(), - log_root, - start_version, - end_version, - )?; - let schema = StructType::new( end_snapshot .schema() diff --git a/kernel/tests/cdf.rs b/kernel/tests/cdf.rs index 2be5324fc..2560dc71d 100644 --- a/kernel/tests/cdf.rs +++ b/kernel/tests/cdf.rs @@ -6,7 +6,7 @@ use delta_kernel::engine::sync::SyncEngine; use itertools::Itertools; use delta_kernel::engine::arrow_data::ArrowEngineData; -use delta_kernel::{DeltaResult, Table, Version}; +use delta_kernel::{DeltaResult, Error, ExpressionRef, Table, Version}; mod common; use common::{load_test_data, to_arrow}; @@ -15,6 +15,7 @@ fn read_cdf_for_table( test_name: impl AsRef, start_version: Version, end_version: impl Into>, + predicate: impl Into>, ) -> DeltaResult> { let test_dir = load_test_data("tests/data", test_name.as_ref()).unwrap(); let test_path = test_dir.path().join(test_name.as_ref()); @@ -34,6 +35,7 @@ fn read_cdf_for_table( let scan = table_changes .into_scan_builder() .with_schema(schema) + .with_predicate(predicate) .build()?; let batches: Vec = scan .execute(engine)? @@ -53,7 +55,7 @@ fn read_cdf_for_table( #[test] fn cdf_with_deletion_vector() -> Result<(), Box> { - let batches = read_cdf_for_table("cdf-table-with-dv", 0, None)?; + let batches = read_cdf_for_table("cdf-table-with-dv", 0, None, None)?; // Each commit performs the following: // 0. Insert 0..=9 // 1. Remove [0, 9] @@ -99,7 +101,7 @@ fn cdf_with_deletion_vector() -> Result<(), Box> { #[test] fn basic_cdf() -> Result<(), Box> { - let batches = read_cdf_for_table("cdf-table", 0, None)?; + let batches = read_cdf_for_table("cdf-table", 0, None, None)?; let mut expected = vec![ "+----+--------+------------+------------------+-----------------+", "| id | name | birthday | _change_type | _commit_version |", @@ -136,7 +138,7 @@ fn basic_cdf() -> Result<(), Box> { #[test] fn cdf_non_partitioned() -> Result<(), Box> { - let batches = read_cdf_for_table("cdf-table-non-partitioned", 0, None)?; + let batches = read_cdf_for_table("cdf-table-non-partitioned", 0, None, None)?; let mut expected = vec![ "+----+--------+------------+-------------------+---------------+--------------+----------------+------------------+-----------------+", "| id | name | birthday | long_field | boolean_field | double_field | smallint_field | _change_type | _commit_version |", @@ -175,7 +177,7 @@ fn cdf_non_partitioned() -> Result<(), Box> { #[test] fn cdf_with_cdc_and_dvs() -> Result<(), Box> { - let batches = read_cdf_for_table("cdf-table-with-cdc-and-dvs", 0, None)?; + let batches = read_cdf_for_table("cdf-table-with-cdc-and-dvs", 0, None, None)?; let mut expected = vec![ "+----+--------------------+------------------+-----------------+", "| id | comment | _change_type | _commit_version |", @@ -229,3 +231,312 @@ fn cdf_with_cdc_and_dvs() -> Result<(), Box> { assert_batches_sorted_eq!(expected, &batches); Ok(()) } + +#[test] +fn simple_cdf_version_ranges() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-simple", 0, 0, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + + let batches = read_cdf_for_table("cdf-table-simple", 1, 1, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | delete | 1 |", + "| 1 | delete | 1 |", + "| 2 | delete | 1 |", + "| 3 | delete | 1 |", + "| 4 | delete | 1 |", + "| 5 | delete | 1 |", + "| 6 | delete | 1 |", + "| 7 | delete | 1 |", + "| 8 | delete | 1 |", + "| 9 | delete | 1 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + + let batches = read_cdf_for_table("cdf-table-simple", 2, 2, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 20 | insert | 2 |", + "| 21 | insert | 2 |", + "| 22 | insert | 2 |", + "| 23 | insert | 2 |", + "| 24 | insert | 2 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + + let batches = read_cdf_for_table("cdf-table-simple", 0, 2, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "| 0 | delete | 1 |", + "| 1 | delete | 1 |", + "| 2 | delete | 1 |", + "| 3 | delete | 1 |", + "| 4 | delete | 1 |", + "| 5 | delete | 1 |", + "| 6 | delete | 1 |", + "| 7 | delete | 1 |", + "| 8 | delete | 1 |", + "| 9 | delete | 1 |", + "| 20 | insert | 2 |", + "| 21 | insert | 2 |", + "| 22 | insert | 2 |", + "| 23 | insert | 2 |", + "| 24 | insert | 2 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn update_operations() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-update-ops", 0, 2, None)?; + // Note: `update_pre` and `update_post` are technically not part of the delta spec, and instead + // should be `update_preimage` and `update_postimage` respectively. However, the tests in + // delta-spark use the post and pre. + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "| 20 | update_pre | 1 |", + "| 21 | update_pre | 1 |", + "| 22 | update_pre | 1 |", + "| 23 | update_pre | 1 |", + "| 24 | update_pre | 1 |", + "| 30 | update_post | 2 |", + "| 31 | update_post | 2 |", + "| 32 | update_post | 2 |", + "| 33 | update_post | 2 |", + "| 34 | update_post | 2 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn false_data_change_is_ignored() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-data-change", 0, 1, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn invalid_range_end_before_start() { + let res = read_cdf_for_table("cdf-table-simple", 1, 0, None); + let expected_msg = + "Failed to build LogSegment: start_version cannot be greater than end_version"; + assert!(matches!(res, Err(Error::Generic(msg)) if msg == expected_msg)); +} + +#[test] +fn invalid_range_start_after_last_version_of_table() { + let res = read_cdf_for_table("cdf-table-simple", 3, 4, None); + let expected_msg = "Expected the first commit to have version 3"; + assert!(matches!(res, Err(Error::Generic(msg)) if msg == expected_msg)); +} + +#[test] +fn partition_table() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-partitioned", 0, 2, None)?; + let mut expected = vec![ + "+----+------+------+------------------+-----------------+", + "| id | text | part | _change_type | _commit_version |", + "+----+------+------+------------------+-----------------+", + "| 0 | old | 0 | insert | 0 |", + "| 1 | old | 1 | insert | 0 |", + "| 2 | old | 0 | insert | 0 |", + "| 3 | old | 1 | insert | 0 |", + "| 4 | old | 0 | insert | 0 |", + "| 5 | old | 1 | insert | 0 |", + "| 3 | old | 1 | delete | 1 |", + "| 1 | old | 1 | update_preimage | 1 |", + "| 1 | new | 1 | update_postimage | 1 |", + "| 0 | old | 0 | delete | 2 |", + "| 2 | old | 0 | delete | 2 |", + "| 4 | old | 0 | delete | 2 |", + "+----+------+------+------------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn backtick_column_names() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-backtick-column-names", 0, None, None)?; + let mut expected = vec![ + "+--------+----------+--------------------------+--------------+-----------------+", + "| id.num | id.num`s | struct_col | _change_type | _commit_version |", + "+--------+----------+--------------------------+--------------+-----------------+", + "| 2 | 10 | {field: 1, field.one: 2} | insert | 0 |", + "| 4 | 10 | {field: 1, field.one: 2} | insert | 0 |", + "| 1 | 10 | {field: 1, field.one: 2} | insert | 1 |", + "| 3 | 10 | {field: 1, field.one: 2} | insert | 1 |", + "| 5 | 10 | {field: 1, field.one: 2} | insert | 1 |", + "+--------+----------+--------------------------+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn unconditional_delete() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-delete-unconditional", 0, None, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "| 0 | delete | 1 |", + "| 1 | delete | 1 |", + "| 2 | delete | 1 |", + "| 3 | delete | 1 |", + "| 4 | delete | 1 |", + "| 5 | delete | 1 |", + "| 6 | delete | 1 |", + "| 7 | delete | 1 |", + "| 8 | delete | 1 |", + "| 9 | delete | 1 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn conditional_delete_all_rows() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-delete-conditional-all-rows", 0, None, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "| 0 | delete | 1 |", + "| 1 | delete | 1 |", + "| 2 | delete | 1 |", + "| 3 | delete | 1 |", + "| 4 | delete | 1 |", + "| 5 | delete | 1 |", + "| 6 | delete | 1 |", + "| 7 | delete | 1 |", + "| 8 | delete | 1 |", + "| 9 | delete | 1 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} + +#[test] +fn conditional_delete_two_rows() -> DeltaResult<()> { + let batches = read_cdf_for_table("cdf-table-delete-conditional-two-rows", 0, None, None)?; + let mut expected = vec![ + "+----+--------------+-----------------+", + "| id | _change_type | _commit_version |", + "+----+--------------+-----------------+", + "| 0 | insert | 0 |", + "| 1 | insert | 0 |", + "| 2 | insert | 0 |", + "| 3 | insert | 0 |", + "| 4 | insert | 0 |", + "| 5 | insert | 0 |", + "| 6 | insert | 0 |", + "| 7 | insert | 0 |", + "| 8 | insert | 0 |", + "| 9 | insert | 0 |", + "| 2 | delete | 1 |", + "| 8 | delete | 1 |", + "+----+--------------+-----------------+", + ]; + sort_lines!(expected); + assert_batches_sorted_eq!(expected, &batches); + Ok(()) +} diff --git a/kernel/tests/data/cdf-table-backtick-column-names.tar.zst b/kernel/tests/data/cdf-table-backtick-column-names.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..a72a65511dd1e479e9b40820edf3293191f23d06 GIT binary patch literal 5690 zcmV-A7RBi(wJ-euSdFm&dMM#XK@b>FJ5$vaTu0}yXxB->d?u`G-Az+|n$IhooTEjJ}4-OK_U{)i_9R`{Gb+9?JEy#&Jqd)S2(%>PoO(P%Up zjR_X*W^lo8yw3G0mYU^x>g`vj{54uvZ9aOX;(4_9_dZIUW^>-xbFLwUPOi)lbu_2Q z1y1#PpYHb+kE$UOJP>*^oZT78M?2r`slE5$UMkh~yJ@#`{1)?W!T5jcW5K|rO-Tkw zI;L_d$wcO0?$J4qdrQYdq2pmP#}B8`Z^@XrG{)>QlZgy4nKO_YQ2z72P8P}G6 z=9ZaEt|6r`mvfR15(o)o#hhX$lgGmjvoR5BHm)(9OsiSRbQ+CALqN!*QV_6hQ654* zg@AAru)}R;&JB!1c>FL)f|Q-g1{!iDu=T zoMl-Cl4Ti*%oQYOXV6|1I`eaO-n)M5d6uvGe9lv^KGlxm_>>x*_H`bm*1a9gV*MS9 z#qVS?nM@{=01Ob=hh%!=c&__*Kc!~tDPHaSDD|peqdb2a5p4%1MY zw$lFB?5D@a2afok$MV0=m6CbE(rt^dg(Df4>QWr&sO!~+!Ty+3>Z|DaFE14XdykHw zO<7(ZhcJ~(N9M5rrk#-E@pv5cyI!f#AX1IVoRcw_WTum`$I=wD1Cy~wnL{R-=@cRy zrVOzpOXuSob2%dinX-JG)Vzog>M+(PT4w`wcuc8lwu>kYmFwxYu zd7KBQW@C>KOefRBtYkWm#vx|&I46&q$D*bz%SWZ*5{c>TQheb1+L zo=@w#{(W%^F#naBeZPD+EUoLhk7ln_JzM8~c6!yLR6D0etGk{@aURP@t606y-c>vr z{lWYcbDe`MLrs3zwh#0f6NR1O8xEk z%S*Z5el{kMWw}k3WhGf#TTe=4xrHptw=By-vMe9v=hMVfOT@G1uVncsBVYnO>$EPCzFqUWzse|zQsXcpUpNdpsd>`g5saR9Mo!a@R7W2w-Kjfyu; zR;nBi812YLn<*`_B|XHeXNyq+bIG&vzg+=t8j)8QMJzL5c7*++Aeb#Fry&fe;o@u| zrVnx4#1o=96I24VJi;eH^vIrXoxMMIeOviomA^V8-o~u3h*6|j>CG@yBpWmm@rTp^ zPCJ*VMNQn3+;nlSi=PGpKoNx~p+d{D7ia>DA{UJiU+l!~cNX~CaVbq-Mf$c@V2Gky z^kSrp6J{GZ{du*)0cjVU6GJo=xX7g7IHI`uoY4IVqlz=tEkf0lf*E-h z_oR4siMT(z1s8*msolxU0e*mqK?$l@K|sX&HYgG9YV;K_%zOb2T3G|eQ&)AQN)4`_ zf)Pj?G5E|lHu=jq{*y^+=t>S69I%SUOiFeLW(&aJfg-u9Y174}61aRjbW_Ex!xkP0 zcPYSYi4A5x6`}yY-bTdXnPVardxaDFNchpbV=&mlg{7v5t`Vk9Ayb6PK*bE02*1vV zx(#$<4nE-Q>g@d?nL$+Zr@|P34Rb*P#Y~*2Gva|}AH@}GH&E90a9=w>bpBVy)%u z0u6)}BTw^Jp|guCjK2VMrv*aJk^*xSt)wM|J3iP?K5f(JY|-az;a4cvL{aOAX2K%u zjT|9BQK*!Pq7DFIfg^w(of#CI08*6_RxlKZJf!BEfJ*@3Ax7X-#eF2K$X_tU10x&` zaL^?Vhh_@?s3&_oY67!)zMu;1DZM-Z{GSXy27P!TjE-wBAE~ksWJsNrqXGZI2 z10d5y5QH}piE`$rD?K=sIMO@YL)V?rd`#s<~y!wrm6 z+8~Z)O#;rbm+z|*Mea(Q#V0+HJ7W`QhX|r@B|>)x4`efjI(2#S6Y0%h14D|;6F3w=o&@!joIk{4qJ>pqK4OG96Z6Cei=bKyiyddz zaEa;=ik%}Bw8cGnd1AzPfm1|x+o4tZq2a@F1RWwY~M=_cE+izO$CeTW+(27*XNY*f}77+U+g z(qxR@mo*9)EUh_Mh^1g?AeTY0b9@`rE?um#B7>R|K5p1Af!vRD# zT*3K)@KwBl2IWoZ)eAI}qN~s{+3;aS(0PPNE=pX*t!(M=s|ZVqP7*f&bmbX)7>O8n z08M#_U_*;T@`iF|fHe`FWnfcp3IgneB*FrOG>{aN0+j*PK=&du2>8N?mJMKPX;-=i zqgt055ClAk!w}W!WPD>MM+c-10wl^?Q%uN;O;ecaUElFr*Z2Lt>!)4cdaegoBr~2k zIj&W?>CmR>4lsZh(+g&1tZtm7y2-!_WEL!PO-${NMi@Cvp042C0tY>UzI4%&eT2NQ z`mt^yu!B%Q23f;UpP;fTO^K?TxxlWwq$Z`GeB1Tjwj)8i@-1b!nJxt)VVuw%0|hXa zl5jv^hXE;3Mw@?Fkv{}0Tu3^^)P0uRNEt%}1lEEZIgIF-a1T`IV+I9y##hT|djNnE zF3zwqh6#nCNU~6plL#>LBbwWd22e>onX*APu-8d@nj8WmdsIm(7%`)Fmc_H`aq^D;tGSM;8CiGL6x;7%cX-60DJ|X zV{2x_>Dj}h3QZY}Kgg{k48(X`cysf@i5=r0k#L@-q99RZs1YLbEXDZKx|8TVHVH zkl9;xz;1jfn+rVlBD{~lgEZJ^5h5(%t#?~ zRih2;TVfIK2sFjWL6IatgmNj6f+46%L4b9$xT@ChnQF;iD8hyuq-`fX=4RS)S2NaVWT2j>f>O~ zn=-W|I3%(YB$(^t(-(jpDJv$xQ~>f+2TYvm1f(N;9Xl8+%%D|fPGK+l-XAC|f5cH& zTqWYr_&^F6Q^2AM%z$7CMHU5iY$`A;7keurJ4~d}D%u7l7(dX&U_mrQQv;8Rwm&;g3j!7etF$e`Fp`-dQqE}5qk|rD84w3- z&D<6XKDZts2FQK)q|Hh4<)F!@-GTEHwJUTo=7cv&q@ovt%GHN(jFcujibW#yZ3&61 z661^+8s%A>!#go}p$=gml^dIhHgKv$c&@N`@q1Gw%VtyLyWs*1AT4kT=-;%U$`(LH zJ$G2xQ6`3T`BE@Swy9sgqNd12k+DVQPZJyD_wan<9$h6_h-23JbNAe1P6*39(FI zNk)X9n;%o$86`5JA2krnS`G)pVl=pED}!|b362n67Jw4u3Y&z&y*o!?s0@iA)?g{R zC!MM)NCjc^$kg32Q75N0%M1Io)|+hn*1@(@e2U|b|LKIH1Gw9^QIJ7F6Pcl5V*A3} zQC2hs=uMM7ZneJWcwO7OG|%(<*6;d@4*y+V3+BhVS{h25ZM!0P8rsucRyyp1#Xs+R zAfuuCuLW~;wOkr%gMBRTXLYsQt+)67Uhp@!t2WmEc^}-<&@gMZpeV}0KP|MMOE z?UnEPQhF>F(`LKgo`!yPS?O@p+1J_EI2`8X*D<)c^XG>`f?k4LdMpUrihr>!(r zOOLHI|MMOA-{(&8{oZq`t>^tTTCM$S6r08KsIS+4wCdw}wy*8Hs^8&1tSytuQh~i- zuvCrLb#CW1UhDXM+jV|+c2?K*-xYhKwp($xjOWr`(6h7KN_){>&^pX(Y^A-lT-pn2 zTWPN_7VPc)EUvgqdyQ!;9sc=0EY{^!MDah5rJ=RY)o%6LtQPC#($Kwn+g93lp$*f} zv$2&9`{FHMLcjH!w$dto{!5RoG#1O#R3&-Z)peLmXP)oawJ-g$JN?sXKO z);%@1d)i84sk&u94W+k4Ole^~h0u!DQe28R`7C&=DF1K$4$mg7=o9UCwPIbLhMv*X z>hw2*!f7iVCi7O#BawP0V1w$fXF z%(vQl!&(|zTj>&5-UrjrGy1EGw$f%$U9a}n{$Tz~LuqW+|9+onEA3;wTzYJ!!(jjS zxjdN1qNTQ|vbbm~-SujJ9_)X;G&G;y_ifkIJjZw2N`JZhANIN0pB`IjZ?;SIYq?x+ zPmghXYU?*Xz2&Yqyh3W%U{V=VyySas?^FCR$DOJ?xAQ4(TrBhT*Xd#Wl}G{F19ekk zr}aTLpvrWnO0gH?p4}bgQf`UJH+hNGji7agZ>f;=yCj3hPapAz2F%lq^$lxoGVQ{X-0eBozDn`Y|RWceT_NZ?^@ZNc^dP&rqJF%6+GSaR0p0_ z(w`rpHo~x8PL)j7cTSoY5khzVP!mU#vm_(P+-NQtV@;?sbiD9IDB{Epfx)N|ImHwi z5fKp)Ns^?{W}yYd7^Do6s-g!H;LvCi;~)w`7>HpILI@!OLI@%PLkJ;6Kn!9Ckyv{D z`(P{=^P?*OTpT0p0oXrxp(*9Xl$xaguK)FKLUHFIt!zN>WsJ}O0J8ybKsW>gnx$ie zJ#zyrj|D(A07#hzFzC;atb=cW1ZnAj$DRS)0w&B43B7OKD2hcSfnsRv$JQD#MFgaUE~7uD562+c}qZn~omF=#sjHQ09-G zULx}&C@T=4pPb&u`Rk`IBu;(DHh$V`rk4S5<2xEeXC040_W0_5y+BMFw!A!I46x>UzaWGG(qLM#^I>)BE|TC-at>8Gs%RAV>c1%mFTP z7;t}GqN$>22;Tq!(gD}0HHK&Y1AwYFAci-70OhPf=!_+R3&1t&QVpOq7s7x`vo_(& zH{lnx$U1?G@d4A1F%4>c$~C}ijId`m038Y*Kmi61_yWLjb2AhGYkq0>#2Z-589+INHwf5G zm(`>nyRjaE7Qr4LzzAda^ni+vK_^f+pTG~LY(R~80IIde0|szJECqOO93Z0}P<_dO z$$@s3s>n!TT~V=y3E4k|F)|EoWikwRZZ=jRnw-lnEexes2&)r&?5bQePeh>blr`Zg z{C9KAyCJ%7)*^|Skl+2+!YqR+NW`}o8HF{C77f}^G+w(G_1`IhM)0m0s2GfrjEB=C zD1diXmkof#=3I0v{?cp;0%}yC!6Ny3siSL5>{eO=ssNVln=%&n7@*l{G&cihR)9!% zc02EtyKyps;UZ}DuvFs)xhE)90jTiNcLic&XgdLn{fgK|%I$IoT0i%N`N$G{KX(R{ zp;Fm1{UYlh#LNegeBswuE6a4Z(Bj{uw|t|y&|B}udynWu#={ZW zaSzjMZSc4rW&_XJ0@;7t@!t#{2&ye}Xv7tqr z8$)9yJ7xpyTkruu6aqB3N&s)6d_crJfqs;U9_*ezX@EptQUN>$Q*&R_)xl0MP(yd0 gz-+jbs5Y8VS}H=*-cpi`F$rA53r15GJ~|tt0==22&Hw-a literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-data-change.tar.zst b/kernel/tests/data/cdf-table-data-change.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..974f7587f07b26656dd366e299ec9cf2958de9ac GIT binary patch literal 4850 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 YTna_UnU7Bv`T|5l6<-EB*U1Hu1}nD#PXGV_ literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-delete-conditional-all-rows.tar.zst b/kernel/tests/data/cdf-table-delete-conditional-all-rows.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..fe38e64b18288f1dd350b3ec5cd7a718775e539d GIT binary patch literal 6188 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 literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-delete-conditional-two-rows.tar.zst b/kernel/tests/data/cdf-table-delete-conditional-two-rows.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..e1037199147e9859e32901188f6394a34d359a71 GIT binary patch literal 6772 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�_&HdcIWvbkJao>I!DvyD6iG^Wl5S!`f9lay?BKWVF|Fyo> zGlGaO?-iTs-~0-+OqF}T@3Fw&?tk?=aJ$*x*ZNwyOqIcI-D>Y=z1<4Bbo#?(*)mlI z``WC9-f!z}{pvQ%y=K3^pTP%fc`Q}#xxUwn)pN7jEO1ta#$~m9_UntuR9Wk{`Cr;D zI4eVGs@$RH{jUt2lR?O5R8-Pu73@i6KPNoD@LZbHHB_ISnv&1VWUB1{{59MAU#~1j zmBC;C_qY6)50kgXw!XHsWU9>VW`F+cd%H4pO7FBj&Gj6=_nInuxBL(Gx7lBoqsm<@ z*Xz%2x818OckB9o<95E=F}Dk5i`Fz+6pYUl-fi2qZYPt;WHOl?@=?Yw3w`Uft)0DOv&#A-_#kI*2HHY8O8 z3f75o4?zyLK1PuIfr>hFm9}ruTyfjr;7B^C@{3}$Ms((Ym&8Z6*iS(NIdp<7s8ZXG zI)!FL0+A6(Qkb+E0t^_0f*_;}l@`_k5}@!v4&*2bLLi1g5C}mCA;b`32qA_LLI^=l zp^?f1{R0Qa21Kn4FeL_*;eWtMlB-w%-wwd{iVOXKSyWyq!(L->APoI$s{u(@TD}3? zsW^c22nY&+ABMdL$c{2K(lNl87QmGN$n*g3{yT|cJXamS06c((?CFp623q$g2Hdo! zI?XbO#>HjM)x%KL4omgI978G>9Bx5%12o9w>=G|jkITaeA>i_7ws%8`0=&9|Ly}v2 z7aH_|pj78bQ?C->0XVqd@vxf1%Cz8&pqT3LANJ65Vb4_z%>&pI{K^cpr~tVLp2qdG zD7KypC#^?mTI?NdQ1rLYmD4e6NJC1DC(?JX?SsgBg;Upi>I)4)+M&!*(XYa(2H@4& zY?=^WrH`+!IIWVP1ndsnh;~Mc7^k`X;0!pw4hRBJK1t2RR_3~tzCB5u^++DvnN5cW zQtb)Q}Du~)Q zbrDUt$Pi^i^NECDPm$V@SU!K5GpBoTuT&uTCF&@G4NRG8y=KF_M@$bY*riM<|AC$I z3Q`F8_xn7ze4I2f?e$R+GDIzKZqSk*6z|=GYV$!Yj$B5E#&B5`MD&+QH{ z@UKh8tM~`dg)qYyAj%*> zrvCj<*#OB4w-+_IB&pb4T^w=Cl}!Rrlu{?h13t6)57Hn9q@)9?Eg)C)ndex@EqLYP z=D=VX9IAnmIEG5xQW6LQ>F-P(8VzzXa84n$0nyO12$7p7ED8hirxZRUnE^Us47X{J zF99T|`tw})Fx!C4>;XPhBNqWSA@rCN!cu#58U6v@9|j?_4ggL_VO*oy)aM(p3g`N* zEkJ)U0D5y}0~i_yPyl`Tbu`5dU^WdP@$dI*gc6vEQ2-Xd+Rx_(Na+ty#ATQ2;-LU0 zD8}Xio~YW`L;?Q|2Pj5Xg1Ql9apIcnjv6w+OKJirnmBz}GB;r8*m*ez;|w6{#Ulcm zcvB-|r6Bohl8sn^O8aw_k#rJcTRP9FsTyIXOFZNJIz8kC1UDT}?^0((3J<0tWV@>Z zr{fOSzp;GZj>Se2JIVj=Io$@U>OG~X%%Enk<}9uNto79*o#YL)S+m!F8vJt0;soe5 z1^|^MCZCx&C1u1|ampG5kYdu`Rs;bAz5sxlw;my)3cy}p?HB-Og`I#nE8z1g8$f0a zV5|)@7=Wt`^}Nvo0^$K=4Gs_%@}E)#LK&%r_-+VNf7-Ac8$80BgwDVvf)XxHFbT?z zBruMUaEA-ZXK)qi3MljVhf46JLj}ZpE7%Tnz?d;`xQhu9z{lTm0}|8@c|7li1GS~< zvadG$ByXS%+a=Rc0QUUVPJcZE;3>7w^7;lqy+1ut0vWxDerf@=u=459SAK2?ob?_<4D+mBTi W&BO%$@@=%heGW5N@M13_xzGZy9_i5l literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-delete-unconditional.tar.zst b/kernel/tests/data/cdf-table-delete-unconditional.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..4e57df8be09aa52bdf81772c50051fdaa6ea18f2 GIT binary patch literal 4785 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 LI;c^e08U5)Sr;3c literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-non-partitioned.tar.zst b/kernel/tests/data/cdf-table-non-partitioned.tar.zst index 2a2f08cf0827f8dc12cfb37495cbb264425d4800..f97e1ea8ac791c8cf5550d9140a09a23e6dbb0e6 100644 GIT binary patch literal 20033 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 VT!t>z1e4K4*e$vkdw_km-aX^*_qYH6 literal 11152 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 literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-simple.tar.zst b/kernel/tests/data/cdf-table-simple.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..0051f05f4f533097e8549a444e689ea54e1df2e6 GIT binary patch literal 5889 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 literal 0 HcmV?d00001 diff --git a/kernel/tests/data/cdf-table-update-ops.tar.zst b/kernel/tests/data/cdf-table-update-ops.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..33134b22b1eb368f7db4b1d88079b2ff5b4fd61f GIT binary patch literal 6282 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 GEO-P0Dx*gL literal 19759 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≧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 OZ`1~`aInAeeGs0i>k5Ma diff --git a/kernel/tests/data/cdf-table-with-dv.tar.zst b/kernel/tests/data/cdf-table-with-dv.tar.zst index 76c9f09daec69f80001f9e92848847665bcd6a31..d3aa6574de324461f82d0daa6e143d193d51079d 100644 GIT binary patch literal 4112 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 O3XnknU{>>wo^yS@)6{1G literal 2310 zcmV+h3HkOYwJ-euSnWpuif=R)M-Y*l0{{R3002e+Ag)UzKp0Q}B7Z9A`o|y=mW`J2 zD4NM`vTmC}%K$vMd9NWJUiI$YQc5XBaPK`;QZKN*ZS~1D~2;#jdWDh7fL=E>JC|7|pd_HDg)M?!{^a4tgE@&!ub)~q;YlE94OOetacPGoIpU*ezN;(D{{!)k?L^ zY4LLG+vJ~Ve_t6+YmOYnd+1qnZj*yMoT?DpB)gA$r)ka3m18I8HhC(hPZg=N`|ft< z;B`VAug?T2q4V^@*X`(pqDPMWrPKNEXl;Zdis3eje;}PF`NvUVs6>hyLN@1(91Jb! z@K9{wpx3X9yKpi;@%aA%|1aSGJN`ew{{#3CWI8hc|Cx{f*EqNW zlVs7lO%mCSBF9(2GMpsU+$Py~CpYJvyiNqWZckN#)oN9@8PmOQhh8^sZd&+mljJ={ zBouvL8E&(4@^liXHLpe+G(dgGxcp_<@WG>18%H`qLy3%-+k7G6q4$NNF~yoAj@qNx z$*J0Aht<>>=ZiCK=Qc}PL@cFI35J!>s8rG-IuVUjQa*%AD58^TO-iZ0H^b&OOHM?i z5=x3_v{J%?BATjglBnIMm9O3=N8ypvTB){4^77jqLecA9A9w16f8K?TcOmaE92IVt zk==JUEp0K@YONWD>AKAnxI4)~Z#1nrx9QS%YO1!$Uqq0PQ-$4otHr9-uC=w&n{&P* zN#fG9bY1A6COm;p@R@(QeWufJ+;H&!nTlzfGo@ITDWz0vYH!7C=$S4~Gq*X*ZZ>5g zQ?46?nKTArP1B%^ak_R@mqxL3{G}8I_uiXvhA&pJtLcU546_;ITX8*0HGwb|Z_ zGls3cG^$~GbJkf~Y;i7WwpD9d&8)>1Q>>|kVy%}_dNmK3LMhMsYarJWG83tH*NN$ON5?4a9 z0TqK|1E}IPEfxfXQQ`*t&;V@$>A83}ijoEtKLd2@r2oqVlu9quh_(>|gIuH#Tw!x$ z7Sz+2{z@O{>2=srG~@Ma^p$h4s*YbZ?OrvVu9^rEu~ikmnvDOeN}JThs;co*HR~8v zgT+PrKg;SF2yWy_InqI@XB;({k&^UfQep*0soY>D#`Nz*1oSFv&jZNj0`MOIBAvJ@ z&jY;9&38uA@ZtgIpqtYxmVl=E+}A01Z%52l|Mcuu+*>wax(2ucsR8esIRCAhWhXrc zU~EKi0WR9G*x1T>OJ}!fv32bXXyv=q1AbsJ)gOQzHB9I#5zK^uGs%;gBOBd0!=hqQ z!|}i>9wDe+Z42~&J5(W-`=j?H%s7O}c5*W2>LMnZh)CdJTi>6VLw!bu;TI$PX7Qht zYzhMbbNbi-u5Hx^s5w6Wtu(EBm-RC_K-h~LAR_qzemg)9F3?nVYILC91^|@Z0WKNf z$Op)F01m>4-}f*F%>ZwF0Hhl?2@9XMJ*Q-5+a~k2cMey6J$aw?LJnAu2S8g3;52}* zR*SI%BBH|pqS!D~WET!dD_*lmz#2n(m|o2Z+<4FeU6j>W&aMX9H>JyDB(!BF5El&+ z^0X9MhmyyV8p+s#9e}{{XDa_hDGgNkYU~}N9YFu)EB}D?;UxB*dy59q-Iv{4Im zJ|I3DJBKqsZOJ^V%cA?;fM_3JX`n9C^Z$)}C2#}!WPs)Y!EZo^)&jK^g7^ShJhsk& zDp@0z15^uU7uyd2@~AkIUr3Y{t@Au`;`e=#Jyoz{L2&>d+%3>BQ+OTe|GAe00(gVI g_1R>pel$X_TOUUMuAP-fp9GehO}KIyuj`DA1|I@qumAu6 diff --git a/kernel/tests/data/cdf-table.tar.zst b/kernel/tests/data/cdf-table.tar.zst index 30868b12ca4249ebc6c00af6c453086dd6ebef94..9bdb37d44baf7087e78bd62fe5bfc2b033634276 100644 GIT binary patch literal 16311 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

=cx6;QwwMuT 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 Q|e}q0xY$d|^j@ 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 literal 9540 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 From 616e9ac5435ecc4055c6c3b3c2a10788ece361cd Mon Sep 17 00:00:00 2001 From: Oussama Saoudi Date: Tue, 14 Jan 2025 16:49:42 -0800 Subject: [PATCH 7/9] feat: Add the in-commit timestamp field to CommitInfo (#581) ## What changes are proposed in this pull request? This PR adds the inCommitTimestamp filed to CommitInfo. This lays the groundwork for supporting ICT for table properties and for writes. We update transaction write tests to ignore the in-commit timestamp field since the write path does not currently support in-commit timestamps. ## How was this change tested? All existing tests pass. --- kernel/src/actions/mod.rs | 8 +++++++- kernel/src/transaction.rs | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/kernel/src/actions/mod.rs b/kernel/src/actions/mod.rs index 630eedc81..d03abc616 100644 --- a/kernel/src/actions/mod.rs +++ b/kernel/src/actions/mod.rs @@ -331,8 +331,13 @@ where struct CommitInfo { /// The time this logical file was created, as milliseconds since the epoch. /// Read: optional, write: required (that is, kernel always writes). - /// If in-commit timestamps are enabled, this is always required. pub(crate) timestamp: Option, + /// The time this logical file was created, as milliseconds since the epoch. Unlike + /// `timestamp`, this field is guaranteed to be monotonically increase with each commit. + /// Note: If in-commit timestamps are enabled, both the following must be true: + /// - The `inCommitTimestamp` field must always be present in CommitInfo. + /// - The CommitInfo action must always be the first one in a commit. + pub(crate) in_commit_timestamp: Option, /// An arbitrary string that identifies the operation associated with this commit. This is /// specified by the engine. Read: optional, write: required (that is, kernel alwarys writes). pub(crate) operation: Option, @@ -694,6 +699,7 @@ mod tests { "commitInfo", StructType::new(vec![ StructField::new("timestamp", DataType::LONG, true), + StructField::new("inCommitTimestamp", DataType::LONG, true), StructField::new("operation", DataType::STRING, true), StructField::new( "operationParameters", diff --git a/kernel/src/transaction.rs b/kernel/src/transaction.rs index c73782f64..6cc7ff38b 100644 --- a/kernel/src/transaction.rs +++ b/kernel/src/transaction.rs @@ -315,6 +315,12 @@ fn generate_commit_info( .get_mut("operationParameters") .ok_or_else(|| Error::missing_column("operationParameters"))? .data_type = hack_data_type; + + // Since writing in-commit timestamps is not supported, we remove the field so it is not + // written to the log + commit_info_data_type + .fields + .shift_remove("inCommitTimestamp"); commit_info_field.data_type = DataType::Struct(commit_info_data_type); let commit_info_evaluator = engine.get_expression_handler().get_evaluator( From 76c65c83f6ffa322ffa0a749b252989880d88c74 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Thu, 16 Jan 2025 05:07:45 -0700 Subject: [PATCH 8/9] refactor: Make [non] nullable struct fields easier to create (#646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changes are proposed in this pull request? A lot of code (especially in tests) calls `StructField::new` with literal values of the `nullable: bool` argument. Booleans are easy to misinterpret (which value means non-null field?) -- and it's hard to read when the third arg is split to its own line in nested expressions such as: ```rust StructField::new( "fileConstantValues", StructType::new([StructField::new( "partitionValues", MapType::new(DataType::STRING, DataType::STRING, true), false, )]), true, ), ``` To improve readability and make the code less error-prone, define two new helper methods/constructors for `StructField`: `nullable` and `not_null`, which create struct fields having the corresponding nullability. ## How was this change tested? No new functionality, and all existing unit tests still pass. To minimize the risk of unfaithful refactoring, the change was made in four steps: 1. Use a multi-file regexp search/replace to convert simple code such like this: ```rust StructField::new("a", DataType::LONG, true) ``` to this: ```rust StructField::nullable("a", DataType::LONG) ``` The exact expression used was: `StructField::new(\([^()]*\), true) → StructField::nullable(\1)`, which ignores any constructor call containing parentheses, to avoid ambiguity. 2. Use the multi-file regexp search/replace `StructField::new(\([^()]*\), false) → StructField::not_null(\1)`, to convert simple use `not_null` call sites (see above for details). 3. Use an interactive multi-file search/replace `StructField::new → StructField::nullable`, relying on IDE parentheses matching to identify calls that pass the literal `true` (first pass). As a safety measure, the resulting code is compiled; all changed call sites fail to compile because of the (now unrecognized) third arg, which can then be deleted after verifying it is the literal `true`. 4. Use the same two-pass process for `StructField::new → StructField::not_null` with literal `false`. Each step is its own commit, for easier verification. --- ffi/src/test_ffi.rs | 7 +- integration-tests/src/main.rs | 4 +- kernel/src/actions/mod.rs | 129 ++++++++---------- kernel/src/actions/schemas.rs | 6 +- kernel/src/engine/arrow_conversion.rs | 3 +- kernel/src/engine/arrow_utils.rs | 119 ++++++++-------- kernel/src/engine/ensure_data_types.rs | 21 ++- kernel/src/scan/data_skipping.rs | 10 +- kernel/src/scan/log_replay.rs | 24 ++-- kernel/src/scan/mod.rs | 42 +++--- kernel/src/schema.rs | 58 ++++---- kernel/src/table_changes/log_replay/tests.rs | 30 ++-- kernel/src/table_changes/mod.rs | 10 +- .../src/table_changes/physical_to_logical.rs | 12 +- kernel/src/table_changes/scan.rs | 6 +- kernel/src/table_changes/scan_file.rs | 42 +++--- kernel/src/transaction.rs | 17 +-- kernel/tests/write.rs | 25 ++-- 18 files changed, 258 insertions(+), 307 deletions(-) diff --git a/ffi/src/test_ffi.rs b/ffi/src/test_ffi.rs index 14eec1b86..55456d7e5 100644 --- a/ffi/src/test_ffi.rs +++ b/ffi/src/test_ffi.rs @@ -25,18 +25,17 @@ pub unsafe extern "C" fn get_testing_kernel_expression() -> Handle arrow::datatypes::Schema { fn create_kernel_schema() -> delta_kernel::schema::Schema { use delta_kernel::schema::{DataType, Schema, StructField}; - let field_a = StructField::new("a", DataType::LONG, false); - let field_b = StructField::new("b", DataType::BOOLEAN, false); + let field_a = StructField::not_null("a", DataType::LONG); + let field_b = StructField::not_null("b", DataType::BOOLEAN); Schema::new(vec![field_a, field_b]) } diff --git a/kernel/src/actions/mod.rs b/kernel/src/actions/mod.rs index d03abc616..2352e0db7 100644 --- a/kernel/src/actions/mod.rs +++ b/kernel/src/actions/mod.rs @@ -524,38 +524,30 @@ mod tests { .project(&[METADATA_NAME]) .expect("Couldn't get metaData field"); - let expected = Arc::new(StructType::new([StructField::new( + let expected = Arc::new(StructType::new([StructField::nullable( "metaData", StructType::new([ - StructField::new("id", DataType::STRING, false), - StructField::new("name", DataType::STRING, true), - StructField::new("description", DataType::STRING, true), - StructField::new( + StructField::not_null("id", DataType::STRING), + StructField::nullable("name", DataType::STRING), + StructField::nullable("description", DataType::STRING), + StructField::not_null( "format", StructType::new([ - StructField::new("provider", DataType::STRING, false), - StructField::new( + StructField::not_null("provider", DataType::STRING), + StructField::not_null( "options", MapType::new(DataType::STRING, DataType::STRING, false), - false, ), ]), - false, ), - StructField::new("schemaString", DataType::STRING, false), - StructField::new( - "partitionColumns", - ArrayType::new(DataType::STRING, false), - false, - ), - StructField::new("createdTime", DataType::LONG, true), - StructField::new( + StructField::not_null("schemaString", DataType::STRING), + StructField::not_null("partitionColumns", ArrayType::new(DataType::STRING, false)), + StructField::nullable("createdTime", DataType::LONG), + StructField::not_null( "configuration", MapType::new(DataType::STRING, DataType::STRING, false), - false, ), ]), - true, )])); assert_eq!(schema, expected); } @@ -566,61 +558,55 @@ mod tests { .project(&[ADD_NAME]) .expect("Couldn't get add field"); - let expected = Arc::new(StructType::new([StructField::new( + let expected = Arc::new(StructType::new([StructField::nullable( "add", StructType::new([ - StructField::new("path", DataType::STRING, false), - StructField::new( + StructField::not_null("path", DataType::STRING), + StructField::not_null( "partitionValues", MapType::new(DataType::STRING, DataType::STRING, true), - false, ), - StructField::new("size", DataType::LONG, false), - StructField::new("modificationTime", DataType::LONG, false), - StructField::new("dataChange", DataType::BOOLEAN, false), - StructField::new("stats", DataType::STRING, true), - StructField::new( + StructField::not_null("size", DataType::LONG), + StructField::not_null("modificationTime", DataType::LONG), + StructField::not_null("dataChange", DataType::BOOLEAN), + StructField::nullable("stats", DataType::STRING), + StructField::nullable( "tags", MapType::new(DataType::STRING, DataType::STRING, false), - true, ), deletion_vector_field(), - StructField::new("baseRowId", DataType::LONG, true), - StructField::new("defaultRowCommitVersion", DataType::LONG, true), - StructField::new("clusteringProvider", DataType::STRING, true), + StructField::nullable("baseRowId", DataType::LONG), + StructField::nullable("defaultRowCommitVersion", DataType::LONG), + StructField::nullable("clusteringProvider", DataType::STRING), ]), - true, )])); assert_eq!(schema, expected); } fn tags_field() -> StructField { - StructField::new( + StructField::nullable( "tags", MapType::new(DataType::STRING, DataType::STRING, false), - true, ) } fn partition_values_field() -> StructField { - StructField::new( + StructField::nullable( "partitionValues", MapType::new(DataType::STRING, DataType::STRING, false), - true, ) } fn deletion_vector_field() -> StructField { - StructField::new( + StructField::nullable( "deletionVector", DataType::struct_type([ - StructField::new("storageType", DataType::STRING, false), - StructField::new("pathOrInlineDv", DataType::STRING, false), - StructField::new("offset", DataType::INTEGER, true), - StructField::new("sizeInBytes", DataType::INTEGER, false), - StructField::new("cardinality", DataType::LONG, false), + StructField::not_null("storageType", DataType::STRING), + StructField::not_null("pathOrInlineDv", DataType::STRING), + StructField::nullable("offset", DataType::INTEGER), + StructField::not_null("sizeInBytes", DataType::INTEGER), + StructField::not_null("cardinality", DataType::LONG), ]), - true, ) } @@ -629,21 +615,20 @@ mod tests { let schema = get_log_schema() .project(&[REMOVE_NAME]) .expect("Couldn't get remove field"); - let expected = Arc::new(StructType::new([StructField::new( + let expected = Arc::new(StructType::new([StructField::nullable( "remove", StructType::new([ - StructField::new("path", DataType::STRING, false), - StructField::new("deletionTimestamp", DataType::LONG, true), - StructField::new("dataChange", DataType::BOOLEAN, false), - StructField::new("extendedFileMetadata", DataType::BOOLEAN, true), + StructField::not_null("path", DataType::STRING), + StructField::nullable("deletionTimestamp", DataType::LONG), + StructField::not_null("dataChange", DataType::BOOLEAN), + StructField::nullable("extendedFileMetadata", DataType::BOOLEAN), partition_values_field(), - StructField::new("size", DataType::LONG, true), + StructField::nullable("size", DataType::LONG), tags_field(), deletion_vector_field(), - StructField::new("baseRowId", DataType::LONG, true), - StructField::new("defaultRowCommitVersion", DataType::LONG, true), + StructField::nullable("baseRowId", DataType::LONG), + StructField::nullable("defaultRowCommitVersion", DataType::LONG), ]), - true, )])); assert_eq!(schema, expected); } @@ -653,20 +638,18 @@ mod tests { let schema = get_log_schema() .project(&[CDC_NAME]) .expect("Couldn't get remove field"); - let expected = Arc::new(StructType::new([StructField::new( + let expected = Arc::new(StructType::new([StructField::nullable( "cdc", StructType::new([ - StructField::new("path", DataType::STRING, false), - StructField::new( + StructField::not_null("path", DataType::STRING), + StructField::not_null( "partitionValues", MapType::new(DataType::STRING, DataType::STRING, true), - false, ), - StructField::new("size", DataType::LONG, false), - StructField::new("dataChange", DataType::BOOLEAN, false), + StructField::not_null("size", DataType::LONG), + StructField::not_null("dataChange", DataType::BOOLEAN), tags_field(), ]), - true, )])); assert_eq!(schema, expected); } @@ -677,14 +660,13 @@ mod tests { .project(&["txn"]) .expect("Couldn't get transaction field"); - let expected = Arc::new(StructType::new([StructField::new( + let expected = Arc::new(StructType::new([StructField::nullable( "txn", StructType::new([ - StructField::new("appId", DataType::STRING, false), - StructField::new("version", DataType::LONG, false), - StructField::new("lastUpdated", DataType::LONG, true), + StructField::not_null("appId", DataType::STRING), + StructField::not_null("version", DataType::LONG), + StructField::nullable("lastUpdated", DataType::LONG), ]), - true, )])); assert_eq!(schema, expected); } @@ -695,25 +677,22 @@ mod tests { .project(&["commitInfo"]) .expect("Couldn't get commitInfo field"); - let expected = Arc::new(StructType::new(vec![StructField::new( + let expected = Arc::new(StructType::new(vec![StructField::nullable( "commitInfo", StructType::new(vec![ - StructField::new("timestamp", DataType::LONG, true), - StructField::new("inCommitTimestamp", DataType::LONG, true), - StructField::new("operation", DataType::STRING, true), - StructField::new( + StructField::nullable("timestamp", DataType::LONG), + StructField::nullable("inCommitTimestamp", DataType::LONG), + StructField::nullable("operation", DataType::STRING), + StructField::nullable( "operationParameters", MapType::new(DataType::STRING, DataType::STRING, false), - true, ), - StructField::new("kernelVersion", DataType::STRING, true), - StructField::new( + StructField::nullable("kernelVersion", DataType::STRING), + StructField::nullable( "engineCommitInfo", MapType::new(DataType::STRING, DataType::STRING, false), - true, ), ]), - true, )])); assert_eq!(schema, expected); } diff --git a/kernel/src/actions/schemas.rs b/kernel/src/actions/schemas.rs index 0588c04d4..aa3b3e47b 100644 --- a/kernel/src/actions/schemas.rs +++ b/kernel/src/actions/schemas.rs @@ -84,20 +84,20 @@ pub(crate) trait GetNullableContainerStructField { // nullable values impl GetNullableContainerStructField for T { fn get_nullable_container_struct_field(name: impl Into) -> StructField { - StructField::new(name, T::to_nullable_container_type(), false) + StructField::not_null(name, T::to_nullable_container_type()) } } // Normal types produce non-nullable fields impl GetStructField for T { fn get_struct_field(name: impl Into) -> StructField { - StructField::new(name, T::to_data_type(), false) + StructField::not_null(name, T::to_data_type()) } } // Option types produce nullable fields impl GetStructField for Option { fn get_struct_field(name: impl Into) -> StructField { - StructField::new(name, T::to_data_type(), true) + StructField::nullable(name, T::to_data_type()) } } diff --git a/kernel/src/engine/arrow_conversion.rs b/kernel/src/engine/arrow_conversion.rs index fbfdb487a..0b905ff3a 100644 --- a/kernel/src/engine/arrow_conversion.rs +++ b/kernel/src/engine/arrow_conversion.rs @@ -263,8 +263,7 @@ mod tests { fn test_metadata_string_conversion() -> DeltaResult<()> { let mut metadata = HashMap::new(); metadata.insert("description", "hello world".to_owned()); - let struct_field = - StructField::new("name", DataType::STRING, false).with_metadata(metadata); + let struct_field = StructField::not_null("name", DataType::STRING).with_metadata(metadata); let arrow_field = ArrowField::try_from(&struct_field)?; let new_metadata = arrow_field.metadata(); diff --git a/kernel/src/engine/arrow_utils.rs b/kernel/src/engine/arrow_utils.rs index a3e184574..06441b9d4 100644 --- a/kernel/src/engine/arrow_utils.rs +++ b/kernel/src/engine/arrow_utils.rs @@ -763,9 +763,9 @@ mod tests { #[test] fn simple_mask_indices() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("s", DataType::STRING, true), - StructField::new("i2", DataType::INTEGER, true), + StructField::not_null("i", DataType::INTEGER), + StructField::nullable("s", DataType::STRING), + StructField::nullable("i2", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -787,8 +787,8 @@ mod tests { #[test] fn ensure_data_types_fails_correctly() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("s", DataType::INTEGER, true), + StructField::not_null("i", DataType::INTEGER), + StructField::nullable("s", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -798,8 +798,8 @@ mod tests { assert!(res.is_err()); let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("s", DataType::STRING, true), + StructField::not_null("i", DataType::INTEGER), + StructField::nullable("s", DataType::STRING), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -811,10 +811,9 @@ mod tests { #[test] fn mask_with_map() { - let requested_schema = Arc::new(StructType::new([StructField::new( + let requested_schema = Arc::new(StructType::new([StructField::not_null( "map", MapType::new(DataType::INTEGER, DataType::STRING, false), - false, )])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new_map( "map", @@ -835,9 +834,9 @@ mod tests { #[test] fn simple_reorder_indices() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("s", DataType::STRING, true), - StructField::new("i2", DataType::INTEGER, true), + StructField::not_null("i", DataType::INTEGER), + StructField::nullable("s", DataType::STRING), + StructField::nullable("i2", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i2", ArrowDataType::Int32, true), @@ -859,9 +858,9 @@ mod tests { #[test] fn simple_nullable_field_missing() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("s", DataType::STRING, true), - StructField::new("i2", DataType::INTEGER, true), + StructField::not_null("i", DataType::INTEGER), + StructField::nullable("s", DataType::STRING), + StructField::nullable("i2", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -882,16 +881,15 @@ mod tests { #[test] fn nested_indices() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "nested", StructType::new([ - StructField::new("int32", DataType::INTEGER, false), - StructField::new("string", DataType::STRING, false), + StructField::not_null("int32", DataType::INTEGER), + StructField::not_null("string", DataType::STRING), ]), - false, ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = nested_parquet_schema(); let (mask_indices, reorder_indices) = @@ -912,16 +910,15 @@ mod tests { #[test] fn nested_indices_reorder() { let requested_schema = Arc::new(StructType::new([ - StructField::new( + StructField::not_null( "nested", StructType::new([ - StructField::new("string", DataType::STRING, false), - StructField::new("int32", DataType::INTEGER, false), + StructField::not_null("string", DataType::STRING), + StructField::not_null("int32", DataType::INTEGER), ]), - false, ), - StructField::new("j", DataType::INTEGER, false), - StructField::new("i", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), + StructField::not_null("i", DataType::INTEGER), ])); let parquet_schema = nested_parquet_schema(); let (mask_indices, reorder_indices) = @@ -942,13 +939,12 @@ mod tests { #[test] fn nested_indices_mask_inner() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "nested", - StructType::new([StructField::new("int32", DataType::INTEGER, false)]), - false, + StructType::new([StructField::not_null("int32", DataType::INTEGER)]), ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = nested_parquet_schema(); let (mask_indices, reorder_indices) = @@ -966,9 +962,9 @@ mod tests { #[test] fn simple_list_mask() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("list", ArrayType::new(DataType::INTEGER, false), false), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("i", DataType::INTEGER), + StructField::not_null("list", ArrayType::new(DataType::INTEGER, false)), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -997,10 +993,9 @@ mod tests { #[test] fn list_skip_earlier_element() { - let requested_schema = Arc::new(StructType::new([StructField::new( + let requested_schema = Arc::new(StructType::new([StructField::not_null( "list", ArrayType::new(DataType::INTEGER, false), - false, )])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -1025,20 +1020,19 @@ mod tests { #[test] fn nested_indices_list() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "list", ArrayType::new( StructType::new([ - StructField::new("int32", DataType::INTEGER, false), - StructField::new("string", DataType::STRING, false), + StructField::not_null("int32", DataType::INTEGER), + StructField::not_null("string", DataType::STRING), ]) .into(), false, ), - false, ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -1077,8 +1071,8 @@ mod tests { #[test] fn nested_indices_unselected_list() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("i", DataType::INTEGER), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -1110,16 +1104,15 @@ mod tests { #[test] fn nested_indices_list_mask_inner() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "list", ArrayType::new( - StructType::new([StructField::new("int32", DataType::INTEGER, false)]).into(), + StructType::new([StructField::not_null("int32", DataType::INTEGER)]).into(), false, ), - false, ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), @@ -1155,20 +1148,19 @@ mod tests { #[test] fn nested_indices_list_mask_inner_reorder() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "list", ArrayType::new( StructType::new([ - StructField::new("string", DataType::STRING, false), - StructField::new("int2", DataType::INTEGER, false), + StructField::not_null("string", DataType::STRING), + StructField::not_null("int2", DataType::INTEGER), ]) .into(), false, ), - false, ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new("i", ArrowDataType::Int32, false), // field 0 @@ -1208,16 +1200,15 @@ mod tests { #[test] fn skipped_struct() { let requested_schema = Arc::new(StructType::new([ - StructField::new("i", DataType::INTEGER, false), - StructField::new( + StructField::not_null("i", DataType::INTEGER), + StructField::not_null( "nested", StructType::new([ - StructField::new("int32", DataType::INTEGER, false), - StructField::new("string", DataType::STRING, false), + StructField::not_null("int32", DataType::INTEGER), + StructField::not_null("string", DataType::STRING), ]), - false, ), - StructField::new("j", DataType::INTEGER, false), + StructField::not_null("j", DataType::INTEGER), ])); let parquet_schema = Arc::new(ArrowSchema::new(vec![ ArrowField::new( @@ -1386,8 +1377,8 @@ mod tests { #[test] fn no_matches() { let requested_schema = Arc::new(StructType::new([ - StructField::new("s", DataType::STRING, true), - StructField::new("i2", DataType::INTEGER, true), + StructField::nullable("s", DataType::STRING), + StructField::nullable("i2", DataType::INTEGER), ])); let nots_field = ArrowField::new("NOTs", ArrowDataType::Utf8, true); let noti2_field = ArrowField::new("NOTi2", ArrowDataType::Int32, true); diff --git a/kernel/src/engine/ensure_data_types.rs b/kernel/src/engine/ensure_data_types.rs index 9b7ea7819..88ff01626 100644 --- a/kernel/src/engine/ensure_data_types.rs +++ b/kernel/src/engine/ensure_data_types.rs @@ -400,36 +400,33 @@ mod tests { #[test] fn ensure_struct() { - let schema = DataType::struct_type([StructField::new( + let schema = DataType::struct_type([StructField::nullable( "a", ArrayType::new( DataType::struct_type([ - StructField::new("w", DataType::LONG, true), - StructField::new("x", ArrayType::new(DataType::LONG, true), true), - StructField::new( + StructField::nullable("w", DataType::LONG), + StructField::nullable("x", ArrayType::new(DataType::LONG, true)), + StructField::nullable( "y", MapType::new(DataType::LONG, DataType::STRING, true), - true, ), - StructField::new( + StructField::nullable( "z", DataType::struct_type([ - StructField::new("n", DataType::LONG, true), - StructField::new("m", DataType::STRING, true), + StructField::nullable("n", DataType::LONG), + StructField::nullable("m", DataType::STRING), ]), - true, ), ]), true, ), - true, )]); let arrow_struct: ArrowDataType = (&schema).try_into().unwrap(); assert!(ensure_data_types(&schema, &arrow_struct, true).is_ok()); let kernel_simple = DataType::struct_type([ - StructField::new("w", DataType::LONG, true), - StructField::new("x", DataType::LONG, true), + StructField::nullable("w", DataType::LONG), + StructField::nullable("x", DataType::LONG), ]); let arrow_simple_ok = ArrowField::new_struct( diff --git a/kernel/src/scan/data_skipping.rs b/kernel/src/scan/data_skipping.rs index 847855d4a..b30711f48 100644 --- a/kernel/src/scan/data_skipping.rs +++ b/kernel/src/scan/data_skipping.rs @@ -59,7 +59,7 @@ impl DataSkippingFilter { physical_predicate: Option<(ExpressionRef, SchemaRef)>, ) -> Option { static PREDICATE_SCHEMA: LazyLock = LazyLock::new(|| { - DataType::struct_type([StructField::new("predicate", DataType::BOOLEAN, true)]) + DataType::struct_type([StructField::nullable("predicate", DataType::BOOLEAN)]) }); static STATS_EXPR: LazyLock = LazyLock::new(|| column_expr!("add.stats")); static FILTER_EXPR: LazyLock = @@ -82,10 +82,10 @@ impl DataSkippingFilter { .transform_struct(&referenced_schema)? .into_owned(); let stats_schema = Arc::new(StructType::new([ - StructField::new("numRecords", DataType::LONG, true), - StructField::new("nullCount", nullcount_schema, true), - StructField::new("minValues", referenced_schema.clone(), true), - StructField::new("maxValues", referenced_schema, true), + StructField::nullable("numRecords", DataType::LONG), + StructField::nullable("nullCount", nullcount_schema), + StructField::nullable("minValues", referenced_schema.clone()), + StructField::nullable("maxValues", referenced_schema), ])); // Skipping happens in several steps: diff --git a/kernel/src/scan/log_replay.rs b/kernel/src/scan/log_replay.rs index fb5c2b0fa..d7f83a4fa 100644 --- a/kernel/src/scan/log_replay.rs +++ b/kernel/src/scan/log_replay.rs @@ -162,21 +162,21 @@ pub(crate) static SCAN_ROW_SCHEMA: LazyLock> = LazyLock::new(|| // Note that fields projected out of a nullable struct must be nullable let partition_values = MapType::new(DataType::STRING, DataType::STRING, true); let file_constant_values = - StructType::new([StructField::new("partitionValues", partition_values, true)]); + StructType::new([StructField::nullable("partitionValues", partition_values)]); let deletion_vector = StructType::new([ - StructField::new("storageType", DataType::STRING, true), - StructField::new("pathOrInlineDv", DataType::STRING, true), - StructField::new("offset", DataType::INTEGER, true), - StructField::new("sizeInBytes", DataType::INTEGER, true), - StructField::new("cardinality", DataType::LONG, true), + StructField::nullable("storageType", DataType::STRING), + StructField::nullable("pathOrInlineDv", DataType::STRING), + StructField::nullable("offset", DataType::INTEGER), + StructField::nullable("sizeInBytes", DataType::INTEGER), + StructField::nullable("cardinality", DataType::LONG), ]); Arc::new(StructType::new([ - StructField::new("path", DataType::STRING, true), - StructField::new("size", DataType::LONG, true), - StructField::new("modificationTime", DataType::LONG, true), - StructField::new("stats", DataType::STRING, true), - StructField::new("deletionVector", deletion_vector, true), - StructField::new("fileConstantValues", file_constant_values, true), + StructField::nullable("path", DataType::STRING), + StructField::nullable("size", DataType::LONG), + StructField::nullable("modificationTime", DataType::LONG), + StructField::nullable("stats", DataType::STRING), + StructField::nullable("deletionVector", deletion_vector), + StructField::nullable("fileConstantValues", file_constant_values), ])) }); diff --git a/kernel/src/scan/mod.rs b/kernel/src/scan/mod.rs index e0d345b56..cd17bca7d 100644 --- a/kernel/src/scan/mod.rs +++ b/kernel/src/scan/mod.rs @@ -820,34 +820,32 @@ mod tests { #[test] fn test_physical_predicate() { let logical_schema = StructType::new(vec![ - StructField::new("a", DataType::LONG, true), - StructField::new("b", DataType::LONG, true).with_metadata([( + StructField::nullable("a", DataType::LONG), + StructField::nullable("b", DataType::LONG).with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_b", )]), - StructField::new("phys_b", DataType::LONG, true).with_metadata([( + StructField::nullable("phys_b", DataType::LONG).with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_c", )]), - StructField::new( + StructField::nullable( "nested", StructType::new(vec![ - StructField::new("x", DataType::LONG, true), - StructField::new("y", DataType::LONG, true).with_metadata([( + StructField::nullable("x", DataType::LONG), + StructField::nullable("y", DataType::LONG).with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_y", )]), ]), - true, ), - StructField::new( + StructField::nullable( "mapped", - StructType::new(vec![StructField::new("n", DataType::LONG, true) + StructType::new(vec![StructField::nullable("n", DataType::LONG) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_n", )])]), - true, ) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), @@ -868,14 +866,14 @@ mod tests { column_expr!("a"), Some(PhysicalPredicate::Some( column_expr!("a").into(), - StructType::new(vec![StructField::new("a", DataType::LONG, true)]).into(), + StructType::new(vec![StructField::nullable("a", DataType::LONG)]).into(), )), ), ( column_expr!("b"), Some(PhysicalPredicate::Some( column_expr!("phys_b").into(), - StructType::new(vec![StructField::new("phys_b", DataType::LONG, true) + StructType::new(vec![StructField::nullable("phys_b", DataType::LONG) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_b", @@ -887,10 +885,9 @@ mod tests { column_expr!("nested.x"), Some(PhysicalPredicate::Some( column_expr!("nested.x").into(), - StructType::new(vec![StructField::new( + StructType::new(vec![StructField::nullable( "nested", - StructType::new(vec![StructField::new("x", DataType::LONG, true)]), - true, + StructType::new(vec![StructField::nullable("x", DataType::LONG)]), )]) .into(), )), @@ -899,14 +896,13 @@ mod tests { column_expr!("nested.y"), Some(PhysicalPredicate::Some( column_expr!("nested.phys_y").into(), - StructType::new(vec![StructField::new( + StructType::new(vec![StructField::nullable( "nested", - StructType::new(vec![StructField::new("phys_y", DataType::LONG, true) + StructType::new(vec![StructField::nullable("phys_y", DataType::LONG) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_y", )])]), - true, )]) .into(), )), @@ -915,14 +911,13 @@ mod tests { column_expr!("mapped.n"), Some(PhysicalPredicate::Some( column_expr!("phys_mapped.phys_n").into(), - StructType::new(vec![StructField::new( + StructType::new(vec![StructField::nullable( "phys_mapped", - StructType::new(vec![StructField::new("phys_n", DataType::LONG, true) + StructType::new(vec![StructField::nullable("phys_n", DataType::LONG) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_n", )])]), - true, ) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), @@ -935,14 +930,13 @@ mod tests { Expression::and(column_expr!("mapped.n"), true), Some(PhysicalPredicate::Some( Expression::and(column_expr!("phys_mapped.phys_n"), true).into(), - StructType::new(vec![StructField::new( + StructType::new(vec![StructField::nullable( "phys_mapped", - StructType::new(vec![StructField::new("phys_n", DataType::LONG, true) + StructType::new(vec![StructField::nullable("phys_n", DataType::LONG) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), "phys_n", )])]), - true, ) .with_metadata([( ColumnMetadataKey::ColumnMappingPhysicalName.as_ref(), diff --git a/kernel/src/schema.rs b/kernel/src/schema.rs index d2ff65193..a4cd44a6a 100644 --- a/kernel/src/schema.rs +++ b/kernel/src/schema.rs @@ -122,6 +122,16 @@ impl StructField { } } + /// Creates a new nullable field + pub fn nullable(name: impl Into, data_type: impl Into) -> Self { + Self::new(name, data_type, true) + } + + /// Creates a new non-nullable field + pub fn not_null(name: impl Into, data_type: impl Into) -> Self { + Self::new(name, data_type, false) + } + pub fn with_metadata( mut self, metadata: impl IntoIterator, impl Into)>, @@ -421,7 +431,7 @@ impl MapType { /// Create a schema assuming the map is stored as a struct with the specified key and value field names pub fn as_struct_schema(&self, key_name: String, val_name: String) -> Schema { StructType::new([ - StructField::new(key_name, self.key_type.clone(), false), + StructField::not_null(key_name, self.key_type.clone()), StructField::new(val_name, self.value_type.clone(), self.value_contains_null), ]) } @@ -1090,69 +1100,61 @@ mod tests { #[test] fn test_depth_checker() { let schema = DataType::struct_type([ - StructField::new( + StructField::nullable( "a", ArrayType::new( DataType::struct_type([ - StructField::new("w", DataType::LONG, true), - StructField::new("x", ArrayType::new(DataType::LONG, true), true), - StructField::new( + StructField::nullable("w", DataType::LONG), + StructField::nullable("x", ArrayType::new(DataType::LONG, true)), + StructField::nullable( "y", MapType::new(DataType::LONG, DataType::STRING, true), - true, ), - StructField::new( + StructField::nullable( "z", DataType::struct_type([ - StructField::new("n", DataType::LONG, true), - StructField::new("m", DataType::STRING, true), + StructField::nullable("n", DataType::LONG), + StructField::nullable("m", DataType::STRING), ]), - true, ), ]), true, ), - true, ), - StructField::new( + StructField::nullable( "b", DataType::struct_type([ - StructField::new("o", ArrayType::new(DataType::LONG, true), true), - StructField::new( + StructField::nullable("o", ArrayType::new(DataType::LONG, true)), + StructField::nullable( "p", MapType::new(DataType::LONG, DataType::STRING, true), - true, ), - StructField::new( + StructField::nullable( "q", DataType::struct_type([ - StructField::new( + StructField::nullable( "s", DataType::struct_type([ - StructField::new("u", DataType::LONG, true), - StructField::new("v", DataType::LONG, true), + StructField::nullable("u", DataType::LONG), + StructField::nullable("v", DataType::LONG), ]), - true, ), - StructField::new("t", DataType::LONG, true), + StructField::nullable("t", DataType::LONG), ]), - true, ), - StructField::new("r", DataType::LONG, true), + StructField::nullable("r", DataType::LONG), ]), - true, ), - StructField::new( + StructField::nullable( "c", MapType::new( DataType::LONG, DataType::struct_type([ - StructField::new("f", DataType::LONG, true), - StructField::new("g", DataType::STRING, true), + StructField::nullable("f", DataType::LONG), + StructField::nullable("g", DataType::STRING), ]), true, ), - true, ), ]); diff --git a/kernel/src/table_changes/log_replay/tests.rs b/kernel/src/table_changes/log_replay/tests.rs index 29e076c07..35c4a99f8 100644 --- a/kernel/src/table_changes/log_replay/tests.rs +++ b/kernel/src/table_changes/log_replay/tests.rs @@ -23,8 +23,8 @@ use std::sync::Arc; fn get_schema() -> StructType { StructType::new([ - StructField::new("id", DataType::INTEGER, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::INTEGER), + StructField::nullable("value", DataType::STRING), ]) } @@ -219,17 +219,17 @@ async fn incompatible_schemas_fail() { // The CDF schema has fields: `id: int` and `value: string`. // This commit has schema with fields: `id: long`, `value: string` and `year: int` (nullable). let schema = StructType::new([ - StructField::new("id", DataType::LONG, true), - StructField::new("value", DataType::STRING, true), - StructField::new("year", DataType::INTEGER, true), + StructField::nullable("id", DataType::LONG), + StructField::nullable("value", DataType::STRING), + StructField::nullable("year", DataType::INTEGER), ]); assert_incompatible_schema(schema, get_schema()).await; // The CDF schema has fields: `id: int` and `value: string`. // This commit has schema with fields: `id: long` and `value: string`. let schema = StructType::new([ - StructField::new("id", DataType::LONG, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::LONG), + StructField::nullable("value", DataType::STRING), ]); assert_incompatible_schema(schema, get_schema()).await; @@ -238,12 +238,12 @@ async fn incompatible_schemas_fail() { // The CDF schema has fields: `id: long` and `value: string`. // This commit has schema with fields: `id: int` and `value: string`. let cdf_schema = StructType::new([ - StructField::new("id", DataType::LONG, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::LONG), + StructField::nullable("value", DataType::STRING), ]); let commit_schema = StructType::new([ - StructField::new("id", DataType::INTEGER, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::INTEGER), + StructField::nullable("value", DataType::STRING), ]); assert_incompatible_schema(cdf_schema, commit_schema).await; @@ -252,16 +252,16 @@ async fn incompatible_schemas_fail() { // The CDF schema has fields: nullable `id` and nullable `value`. // This commit has schema with fields: non-nullable `id` and nullable `value`. let schema = StructType::new([ - StructField::new("id", DataType::LONG, false), - StructField::new("value", DataType::STRING, true), + StructField::not_null("id", DataType::LONG), + StructField::nullable("value", DataType::STRING), ]); assert_incompatible_schema(schema, get_schema()).await; // The CDF schema has fields: `id: int` and `value: string`. // This commit has schema with fields:`id: string` and `value: string`. let schema = StructType::new([ - StructField::new("id", DataType::STRING, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::STRING), + StructField::nullable("value", DataType::STRING), ]); assert_incompatible_schema(schema, get_schema()).await; diff --git a/kernel/src/table_changes/mod.rs b/kernel/src/table_changes/mod.rs index dad2f4e9b..a855668d8 100644 --- a/kernel/src/table_changes/mod.rs +++ b/kernel/src/table_changes/mod.rs @@ -60,9 +60,9 @@ static ADD_CHANGE_TYPE: &str = "insert"; static REMOVE_CHANGE_TYPE: &str = "delete"; static CDF_FIELDS: LazyLock<[StructField; 3]> = LazyLock::new(|| { [ - StructField::new(CHANGE_TYPE_COL_NAME, DataType::STRING, false), - StructField::new(COMMIT_VERSION_COL_NAME, DataType::LONG, false), - StructField::new(COMMIT_TIMESTAMP_COL_NAME, DataType::TIMESTAMP, false), + StructField::not_null(CHANGE_TYPE_COL_NAME, DataType::STRING), + StructField::not_null(COMMIT_VERSION_COL_NAME, DataType::LONG), + StructField::not_null(COMMIT_TIMESTAMP_COL_NAME, DataType::TIMESTAMP), ] }); @@ -316,8 +316,8 @@ mod tests { let engine = Box::new(SyncEngine::new()); let table = Table::try_from_uri(path).unwrap(); let expected_schema = [ - StructField::new("part", DataType::INTEGER, true), - StructField::new("id", DataType::INTEGER, true), + StructField::nullable("part", DataType::INTEGER), + StructField::nullable("id", DataType::INTEGER), ] .into_iter() .chain(CDF_FIELDS.clone()); diff --git a/kernel/src/table_changes/physical_to_logical.rs b/kernel/src/table_changes/physical_to_logical.rs index bc8488081..a953048a9 100644 --- a/kernel/src/table_changes/physical_to_logical.rs +++ b/kernel/src/table_changes/physical_to_logical.rs @@ -69,7 +69,7 @@ pub(crate) fn scan_file_physical_schema( physical_schema: &StructType, ) -> SchemaRef { if scan_file.scan_type == CdfScanFileType::Cdc { - let change_type = StructField::new(CHANGE_TYPE_COL_NAME, DataType::STRING, false); + let change_type = StructField::not_null(CHANGE_TYPE_COL_NAME, DataType::STRING); let fields = physical_schema.fields().cloned().chain(Some(change_type)); StructType::new(fields).into() } else { @@ -104,11 +104,11 @@ mod tests { commit_timestamp: 1234, }; let logical_schema = StructType::new([ - StructField::new("id", DataType::STRING, true), - StructField::new("age", DataType::LONG, false), - StructField::new(CHANGE_TYPE_COL_NAME, DataType::STRING, false), - StructField::new(COMMIT_VERSION_COL_NAME, DataType::LONG, false), - StructField::new(COMMIT_TIMESTAMP_COL_NAME, DataType::TIMESTAMP, false), + StructField::nullable("id", DataType::STRING), + StructField::not_null("age", DataType::LONG), + StructField::not_null(CHANGE_TYPE_COL_NAME, DataType::STRING), + StructField::not_null(COMMIT_VERSION_COL_NAME, DataType::LONG), + StructField::not_null(COMMIT_TIMESTAMP_COL_NAME, DataType::TIMESTAMP), ]); let all_fields = vec![ ColumnType::Selected("id".to_string()), diff --git a/kernel/src/table_changes/scan.rs b/kernel/src/table_changes/scan.rs index 9b0ba3067..dffd40f68 100644 --- a/kernel/src/table_changes/scan.rs +++ b/kernel/src/table_changes/scan.rs @@ -420,8 +420,8 @@ mod tests { assert_eq!( scan.logical_schema, StructType::new([ - StructField::new("id", DataType::INTEGER, true), - StructField::new("_commit_version", DataType::LONG, false), + StructField::nullable("id", DataType::INTEGER), + StructField::not_null("_commit_version", DataType::LONG), ]) .into() ); @@ -429,7 +429,7 @@ mod tests { scan.physical_predicate, PhysicalPredicate::Some( predicate, - StructType::new([StructField::new("id", DataType::INTEGER, true),]).into() + StructType::new([StructField::nullable("id", DataType::INTEGER),]).into() ) ); } diff --git a/kernel/src/table_changes/scan_file.rs b/kernel/src/table_changes/scan_file.rs index cc4514186..f428e09df 100644 --- a/kernel/src/table_changes/scan_file.rs +++ b/kernel/src/table_changes/scan_file.rs @@ -176,37 +176,37 @@ impl RowVisitor for CdfScanFileVisitor<'_, T> { pub(crate) fn cdf_scan_row_schema() -> SchemaRef { static CDF_SCAN_ROW_SCHEMA: LazyLock> = LazyLock::new(|| { let deletion_vector = StructType::new([ - StructField::new("storageType", DataType::STRING, true), - StructField::new("pathOrInlineDv", DataType::STRING, true), - StructField::new("offset", DataType::INTEGER, true), - StructField::new("sizeInBytes", DataType::INTEGER, true), - StructField::new("cardinality", DataType::LONG, true), + StructField::nullable("storageType", DataType::STRING), + StructField::nullable("pathOrInlineDv", DataType::STRING), + StructField::nullable("offset", DataType::INTEGER), + StructField::nullable("sizeInBytes", DataType::INTEGER), + StructField::nullable("cardinality", DataType::LONG), ]); let partition_values = MapType::new(DataType::STRING, DataType::STRING, true); let file_constant_values = - StructType::new([StructField::new("partitionValues", partition_values, true)]); + StructType::new([StructField::nullable("partitionValues", partition_values)]); let add = StructType::new([ - StructField::new("path", DataType::STRING, true), - StructField::new("deletionVector", deletion_vector.clone(), true), - StructField::new("fileConstantValues", file_constant_values.clone(), true), + StructField::nullable("path", DataType::STRING), + StructField::nullable("deletionVector", deletion_vector.clone()), + StructField::nullable("fileConstantValues", file_constant_values.clone()), ]); let remove = StructType::new([ - StructField::new("path", DataType::STRING, true), - StructField::new("deletionVector", deletion_vector, true), - StructField::new("fileConstantValues", file_constant_values.clone(), true), + StructField::nullable("path", DataType::STRING), + StructField::nullable("deletionVector", deletion_vector), + StructField::nullable("fileConstantValues", file_constant_values.clone()), ]); let cdc = StructType::new([ - StructField::new("path", DataType::STRING, true), - StructField::new("fileConstantValues", file_constant_values, true), + StructField::nullable("path", DataType::STRING), + StructField::nullable("fileConstantValues", file_constant_values), ]); Arc::new(StructType::new([ - StructField::new("add", add, true), - StructField::new("remove", remove, true), - StructField::new("cdc", cdc, true), - StructField::new("timestamp", DataType::LONG, false), - StructField::new("commit_version", DataType::LONG, false), + StructField::nullable("add", add), + StructField::nullable("remove", remove), + StructField::nullable("cdc", cdc), + StructField::not_null("timestamp", DataType::LONG), + StructField::not_null("commit_version", DataType::LONG), ])) }); CDF_SCAN_ROW_SCHEMA.clone() @@ -334,8 +334,8 @@ mod tests { ) .unwrap(); let table_schema = StructType::new([ - StructField::new("id", DataType::INTEGER, true), - StructField::new("value", DataType::STRING, true), + StructField::nullable("id", DataType::INTEGER), + StructField::nullable("value", DataType::STRING), ]); let scan_data = table_changes_action_iter( Arc::new(engine), diff --git a/kernel/src/transaction.rs b/kernel/src/transaction.rs index 6cc7ff38b..d74c2456a 100644 --- a/kernel/src/transaction.rs +++ b/kernel/src/transaction.rs @@ -277,10 +277,9 @@ fn generate_commit_info( // HACK (part 1/2): since we don't have proper map support, we create a literal struct with // one null field to create data that serializes as "operationParameters": {} Expression::literal(Scalar::Struct(StructData::try_new( - vec![StructField::new( + vec![StructField::nullable( "operation_parameter_int", DataType::INTEGER, - true, )], vec![Scalar::Null(DataType::INTEGER)], )?)), @@ -304,10 +303,9 @@ fn generate_commit_info( }; let engine_commit_info_schema = commit_info_data_type.project_as_struct(&["engineCommitInfo"])?; - let hack_data_type = DataType::Struct(Box::new(StructType::new(vec![StructField::new( + let hack_data_type = DataType::Struct(Box::new(StructType::new(vec![StructField::nullable( "hack_operation_parameter_int", DataType::INTEGER, - true, )]))); commit_info_data_type @@ -677,15 +675,14 @@ mod tests { fn test_write_metadata_schema() { let schema = get_write_metadata_schema(); let expected = StructType::new(vec![ - StructField::new("path", DataType::STRING, false), - StructField::new( + StructField::not_null("path", DataType::STRING), + StructField::not_null( "partitionValues", MapType::new(DataType::STRING, DataType::STRING, true), - false, ), - StructField::new("size", DataType::LONG, false), - StructField::new("modificationTime", DataType::LONG, false), - StructField::new("dataChange", DataType::BOOLEAN, false), + StructField::not_null("size", DataType::LONG), + StructField::not_null("modificationTime", DataType::LONG), + StructField::not_null("dataChange", DataType::BOOLEAN), ]); assert_eq!(*schema, expected.into()); } diff --git a/kernel/tests/write.rs b/kernel/tests/write.rs index e62f8fd7c..2ee6dfdd5 100644 --- a/kernel/tests/write.rs +++ b/kernel/tests/write.rs @@ -147,10 +147,9 @@ async fn test_commit_info() -> Result<(), Box> { let (store, engine, table_location) = setup("test_table", true); // create a simple table: one int column named 'number' - let schema = Arc::new(StructType::new(vec![StructField::new( + let schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); let table = create_table(store.clone(), table_location, schema, &[]).await?; @@ -201,10 +200,9 @@ async fn test_empty_commit() -> Result<(), Box> { let (store, engine, table_location) = setup("test_table", true); // create a simple table: one int column named 'number' - let schema = Arc::new(StructType::new(vec![StructField::new( + let schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); let table = create_table(store.clone(), table_location, schema, &[]).await?; @@ -224,10 +222,9 @@ async fn test_invalid_commit_info() -> Result<(), Box> { let (store, engine, table_location) = setup("test_table", true); // create a simple table: one int column named 'number' - let schema = Arc::new(StructType::new(vec![StructField::new( + let schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); let table = create_table(store.clone(), table_location, schema, &[]).await?; @@ -336,10 +333,9 @@ async fn test_append() -> Result<(), Box> { let (store, engine, table_location) = setup("test_table", true); // create a simple table: one int column named 'number' - let schema = Arc::new(StructType::new(vec![StructField::new( + let schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); let table = create_table(store.clone(), table_location, schema.clone(), &[]).await?; @@ -466,13 +462,12 @@ async fn test_append_partitioned() -> Result<(), Box> { // create a simple partitioned table: one int column named 'number', partitioned by string // column named 'partition' let table_schema = Arc::new(StructType::new(vec![ - StructField::new("number", DataType::INTEGER, true), - StructField::new("partition", DataType::STRING, true), + StructField::nullable("number", DataType::INTEGER), + StructField::nullable("partition", DataType::STRING), ])); - let data_schema = Arc::new(StructType::new(vec![StructField::new( + let data_schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); let table = create_table( store.clone(), @@ -611,16 +606,14 @@ async fn test_append_invalid_schema() -> Result<(), Box> let (store, engine, table_location) = setup("test_table", true); // create a simple table: one int column named 'number' - let table_schema = Arc::new(StructType::new(vec![StructField::new( + let table_schema = Arc::new(StructType::new(vec![StructField::nullable( "number", DataType::INTEGER, - true, )])); // incompatible data schema: one string column named 'string' - let data_schema = Arc::new(StructType::new(vec![StructField::new( + let data_schema = Arc::new(StructType::new(vec![StructField::nullable( "string", DataType::STRING, - true, )])); let table = create_table(store.clone(), table_location, table_schema.clone(), &[]).await?; From 849412602e2d9fb4c4bab88845ea982316aab63f Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Thu, 16 Jan 2025 15:44:17 -0700 Subject: [PATCH 9/9] Make eval_sql_where available to DefaultPredicateEvaluator (#627) ## What changes are proposed in this pull request? Parquet footer skipping code includes (and uses) a helpful `eval_sql_where` method that handles NULL values in comparisons gracefully, by injecting null checking automatically into the predicate's evaluation. It turns out that capability is also useful for the other predicate evaluator implementations (especially now that partition pruning will likely rely on the default predicate evaluator). So we generalize the logic as the provided method `PredicateEvaluator::eval_sql_where`. In order to support that method, we also declare a new `eval_scalar_is_null` trait method, with appropriate implementations. This has the side effect adding support for literal null checks -- previously, only columns could be null-checked. ## How was this change tested? Replace the existing unit test for the parquet skipping evaluator with adapted versions for the default and stats skipping predicate evaluator, which respectively verify that the provided method works correctly in both bool-output and expression-output cases. The parquet skipping module version is removed because it is redundant -- the default evaluator exercises boolean output, and the data skipping evaluator exercises column resolution. --- .../src/engine/parquet_row_group_skipping.rs | 5 +- kernel/src/expressions/mod.rs | 11 + kernel/src/predicates/mod.rs | 192 ++++++++++++++++-- .../src/predicates/parquet_stats_skipping.rs | 119 +---------- .../parquet_stats_skipping/tests.rs | 125 +----------- kernel/src/predicates/tests.rs | 83 +++++++- kernel/src/scan/data_skipping.rs | 17 +- kernel/src/scan/data_skipping/tests.rs | 108 +++++++++- kernel/src/scan/mod.rs | 27 +-- 9 files changed, 393 insertions(+), 294 deletions(-) diff --git a/kernel/src/engine/parquet_row_group_skipping.rs b/kernel/src/engine/parquet_row_group_skipping.rs index 20eea0acb..0adae6c4b 100644 --- a/kernel/src/engine/parquet_row_group_skipping.rs +++ b/kernel/src/engine/parquet_row_group_skipping.rs @@ -1,8 +1,6 @@ //! An implementation of parquet row group skipping using data skipping predicates over footer stats. -use crate::predicates::parquet_stats_skipping::{ - ParquetStatsProvider, ParquetStatsSkippingFilter as _, -}; use crate::expressions::{ColumnName, Expression, Scalar, UnaryExpression, BinaryExpression, VariadicExpression}; +use crate::predicates::parquet_stats_skipping::ParquetStatsProvider; use crate::schema::{DataType, PrimitiveType}; use chrono::{DateTime, Days}; use parquet::arrow::arrow_reader::ArrowReaderBuilder; @@ -57,6 +55,7 @@ impl<'a> RowGroupFilter<'a> { /// Applies a filtering predicate to a row group. Return value false means to skip it. fn apply(row_group: &'a RowGroupMetaData, predicate: &Expression) -> bool { + use crate::predicates::PredicateEvaluator as _; RowGroupFilter::new(row_group, predicate).eval_sql_where(predicate) != Some(false) } diff --git a/kernel/src/expressions/mod.rs b/kernel/src/expressions/mod.rs index 620142679..47d35afa1 100644 --- a/kernel/src/expressions/mod.rs +++ b/kernel/src/expressions/mod.rs @@ -47,6 +47,17 @@ pub enum BinaryOperator { } impl BinaryOperator { + /// True if this is a comparison for which NULL input always produces NULL output + pub(crate) fn is_null_intolerant_comparison(&self) -> bool { + use BinaryOperator::*; + match self { + Plus | Minus | Multiply | Divide => false, // not a comparison + LessThan | LessThanOrEqual | GreaterThan | GreaterThanOrEqual => true, + Equal | NotEqual => true, + Distinct | In | NotIn => false, // tolerates NULL input + } + } + /// Returns `` (if any) such that `B A` is equivalent to `A B`. pub(crate) fn commute(&self) -> Option { use BinaryOperator::*; diff --git a/kernel/src/predicates/mod.rs b/kernel/src/predicates/mod.rs index f13ed9a3b..54004db6f 100644 --- a/kernel/src/predicates/mod.rs +++ b/kernel/src/predicates/mod.rs @@ -20,7 +20,21 @@ mod tests; /// /// Because inversion (`NOT` operator) has special semantics and can often be optimized away by /// pushing it down, most methods take an `inverted` flag. That allows operations like -/// [`UnaryOperator::Not`] to simply evaluate their operand with a flipped `inverted` flag, +/// [`UnaryOperator::Not`] to simply evaluate their operand with a flipped `inverted` flag, and +/// greatly simplifies the implementations of most operators (other than those which have to +/// directly implement NOT semantics, which are unavoidably complex in that regard). +/// +/// # Parameterized output type +/// +/// The types involved in predicate evaluation are parameterized and implementation-specific. For +/// example, [`crate::engine::parquet_stats_skipping::ParquetStatsProvider`] directly evaluates the +/// predicate over parquet footer stats and returns boolean results, while +/// [`crate::scan::data_skipping::DataSkippingPredicateCreator`] instead transforms the input +/// predicate expression to a data skipping predicate expresion that the engine can evaluated +/// directly against Delta data skipping stats during log replay. Although this approach is harder +/// to read and reason about at first, the majority of expressions can be implemented generically, +/// which greatly reduces redundancy and ensures that all flavors of predicate evaluation have the +/// same semantics. /// /// # NULL and error semantics /// @@ -44,6 +58,9 @@ mod tests; pub(crate) trait PredicateEvaluator { type Output; + /// A (possibly inverted) scalar NULL test, e.g. ` IS [NOT] NULL`. + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option; + /// A (possibly inverted) boolean scalar value, e.g. `[NOT] `. fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option; @@ -123,14 +140,19 @@ pub(crate) trait PredicateEvaluator { fn eval_unary(&self, op: UnaryOperator, expr: &Expr, inverted: bool) -> Option { match op { UnaryOperator::Not => self.eval_expr(expr, !inverted), - UnaryOperator::IsNull => { - // Data skipping only supports IS [NOT] NULL over columns (not expressions) - let Expr::Column(col) = expr else { + UnaryOperator::IsNull => match expr { + // WARNING: Only literals and columns can be safely null-checked. Attempting to + // null-check an expressions such as `a < 10` could wrongly produce FALSE in case + // `a` is just plain missing (rather than known to be NULL. A missing-value can + // arise e.g. if data skipping encounters a column with missing stats, or if + // partition pruning encounters a non-partition column. + Expr::Literal(val) => self.eval_scalar_is_null(val, inverted), + Expr::Column(col) => self.eval_is_null(col, inverted), + _ => { debug!("Unsupported operand: IS [NOT] NULL: {expr:?}"); - return None; - }; - self.eval_is_null(col, inverted) - } + None + } + }, } } @@ -229,12 +251,137 @@ pub(crate) trait PredicateEvaluator { Variadic(VariadicExpression { op, exprs }) => self.eval_variadic(*op, exprs, inverted), } } + + /// Evaluates a predicate with SQL WHERE semantics. + /// + /// By default, [`eval_expr`] behaves badly for comparisons involving NULL columns (e.g. `a < + /// 10` when `a` is NULL), because the comparison correctly evaluates to NULL, but NULL + /// expressions are interpreted as "stats missing" (= cannot skip). This ambiguity can "poison" + /// the entire expression, causing it to return NULL instead of FALSE that would allow skipping: + /// + /// ```text + /// WHERE a < 10 -- NULL (can't skip file) + /// WHERE a < 10 AND TRUE -- NULL (can't skip file) + /// WHERE a < 10 OR FALSE -- NULL (can't skip file) + /// ``` + /// + /// Meanwhile, SQL WHERE semantics only keeps rows for which the filter evaluates to + /// TRUE (discarding rows that evaluate to FALSE or NULL): + /// + /// ```text + /// WHERE a < 10 -- NULL (discard row) + /// WHERE a < 10 AND TRUE -- NULL (discard row) + /// WHERE a < 10 OR FALSE -- NULL (discard row) + /// ``` + /// + /// Conceptually, the behavior difference between data skipping and SQL WHERE semantics can be + /// addressed by evaluating with null-safe semantics, as if by ` IS NOT NULL AND `: + /// + /// ```text + /// WHERE (a < 10) IS NOT NULL AND (a < 10) -- FALSE (skip file) + /// WHERE (a < 10 AND TRUE) IS NOT NULL AND (a < 10 AND TRUE) -- FALSE (skip file) + /// WHERE (a < 10 OR FALSE) IS NOT NULL AND (a < 10 OR FALSE) -- FALSE (skip file) + /// ``` + /// + /// HOWEVER, we cannot safely NULL-check the result of an arbitrary data skipping predicate + /// because an expression will also produce NULL if the value is just plain missing (e.g. data + /// skipping over a column that lacks stats), and if that NULL should propagate all the way to + /// top-level, it would be wrongly interpreted as FALSE (= skippable). + /// + /// To prevent wrong data skipping, the predicate evaluator always returns NULL for a NULL check + /// over anything except for literals and columns with known values. So we must push the NULL + /// check down through supported operations (AND as well as null-intolerant comparisons like + /// `<`, `!=`, etc) until it reaches columns and literals where it can do some good, e.g.: + /// + /// ```text + /// WHERE a < 10 AND (b < 20 OR c < 30) + /// ``` + /// + /// would conceptually be interpreted as + /// + /// ```text + /// WHERE + /// (a < 10 AND (b < 20 OR c < 30)) IS NOT NULL AND + /// (a < 10 AND (b < 20 OR c < 30)) + /// ``` + /// + /// We then push the NULL check down through the top-level AND: + /// + /// ```text + /// WHERE + /// (a < 10 IS NOT NULL AND a < 10) AND + /// ((b < 20 OR c < 30) IS NOT NULL AND (b < 20 OR c < 30)) + /// ``` + /// + /// and attempt to push it further into the `a < 10` and `OR` clauses: + /// + /// ```text + /// WHERE + /// (a IS NOT NULL AND 10 IS NOT NULL AND a < 10) AND + /// (b < 20 OR c < 30) + /// ``` + /// + /// Any time the push-down reaches an operator that does not support push-down (such as OR), we + /// simply drop the NULL check. This way, the top-level NULL check only applies to + /// sub-expressions that can safely implement it, while ignoring other sub-expressions. The + /// unsupported sub-expressions could produce nulls at runtime that prevent skipping, but false + /// positives are OK -- the query will still correctly filter out the unwanted rows that result. + /// + /// At expression evaluation time, a NULL value of `a` (from our example) would evaluate as: + /// + /// ```text + /// AND(..., AND(a IS NOT NULL, 10 IS NOT NULL, a < 10), ...) + /// AND(..., AND(FALSE, TRUE, NULL), ...) + /// AND(..., FALSE, ...) + /// FALSE + /// ``` + /// + /// While a non-NULL value of `a` would instead evaluate as: + /// + /// ```text + /// AND(..., AND(a IS NOT NULL, 10 IS NOT NULL, a < 10), ...) + /// AND(..., AND(TRUE, TRUE, ), ...) + /// AND(..., , ...) + /// ``` + /// + /// And a missing value for `a` would safely disable the clause: + /// + /// ```text + /// AND(..., AND(a IS NOT NULL, 10 IS NOT NULL, a < 10), ...) + /// AND(..., AND(NULL, TRUE, NULL), ...) + /// AND(..., NULL, ...) + /// ``` + fn eval_sql_where(&self, filter: &Expr) -> Option { + use Expr::{Binary, Variadic}; + match filter { + Variadic(v) => { + // Recursively invoke `eval_sql_where` instead of the usual `eval_expr` for AND/OR. + let exprs = v.exprs.iter().map(|expr| self.eval_sql_where(expr)); + self.finish_eval_variadic(v.op, exprs, false) + } + Binary(BinaryExpression { op, left, right }) if op.is_null_intolerant_comparison() => { + // Perform a nullsafe comparison instead of the usual `eval_binary` + let exprs = [ + self.eval_unary(UnaryOperator::IsNull, left, true), + self.eval_unary(UnaryOperator::IsNull, right, true), + self.eval_binary(*op, left, right, false), + ]; + self.finish_eval_variadic(VariadicOperator::And, exprs, false) + } + _ => self.eval_expr(filter, false), + } + } } /// A collection of provided methods from the [`PredicateEvaluator`] trait, factored out to allow -/// reuse by the different predicate evaluator implementations. +/// reuse by multiple bool-output predicate evaluator implementations. pub(crate) struct PredicateEvaluatorDefaults; impl PredicateEvaluatorDefaults { + /// Directly null-tests a scalar. See [`PredicateEvaluator::eval_scalar_is_null`]. + pub(crate) fn eval_scalar_is_null(val: &Scalar, inverted: bool) -> Option { + Some(val.is_null() != inverted) + } + /// Directly evaluates a boolean scalar. See [`PredicateEvaluator::eval_scalar`]. pub(crate) fn eval_scalar(val: &Scalar, inverted: bool) -> Option { match val { @@ -326,6 +473,14 @@ impl ResolveColumnAsScalar for UnimplementedColumnResolver { } } +// Used internally and by some tests +pub(crate) struct EmptyColumnResolver; +impl ResolveColumnAsScalar for EmptyColumnResolver { + fn resolve_column(&self, _col: &ColumnName) -> Option { + None + } +} + // In testing, it is convenient to just build a hashmap of scalar values. #[cfg(test)] impl ResolveColumnAsScalar for std::collections::HashMap { @@ -358,13 +513,17 @@ impl From for DefaultPredicateEvaluator PredicateEvaluator for DefaultPredicateEvaluator { type Output = bool; + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option { + PredicateEvaluatorDefaults::eval_scalar_is_null(val, inverted) + } + fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option { PredicateEvaluatorDefaults::eval_scalar(val, inverted) } fn eval_is_null(&self, col: &ColumnName, inverted: bool) -> Option { let col = self.resolve_column(col)?; - Some(matches!(col, Scalar::Null(_)) != inverted) + self.eval_scalar_is_null(&col, inverted) } fn eval_lt(&self, col: &ColumnName, val: &Scalar) -> Option { @@ -428,12 +587,6 @@ impl PredicateEvaluator for DefaultPredicateEvaluator< /// example, comparisons involving a column are converted into comparisons over that column's /// min/max stats, and NULL checks are converted into comparisons involving the column's nullcount /// and rowcount stats. -/// -/// The types involved in these operations are parameterized and implementation-specific. For -/// example, [`crate::engine::parquet_stats_skipping::ParquetStatsProvider`] directly evaluates data -/// skipping expressions and returnss boolean results, while -/// [`crate::scan::data_skipping::DataSkippingPredicateCreator`] instead converts the input -/// predicate to a data skipping predicate that can be evaluated directly later. pub(crate) trait DataSkippingPredicateEvaluator { /// The output type produced by this expression evaluator type Output; @@ -454,6 +607,9 @@ pub(crate) trait DataSkippingPredicateEvaluator { /// Retrieves the row count of a column (parquet footers always include this stat). fn get_rowcount_stat(&self) -> Option; + /// See [`PredicateEvaluator::eval_scalar_is_null`] + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option; + /// See [`PredicateEvaluator::eval_scalar`] fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option; @@ -589,6 +745,10 @@ pub(crate) trait DataSkippingPredicateEvaluator { impl PredicateEvaluator for T { type Output = T::Output; + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option { + self.eval_scalar_is_null(val, inverted) + } + fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option { self.eval_scalar(val, inverted) } diff --git a/kernel/src/predicates/parquet_stats_skipping.rs b/kernel/src/predicates/parquet_stats_skipping.rs index a8c679d69..ff7536f40 100644 --- a/kernel/src/predicates/parquet_stats_skipping.rs +++ b/kernel/src/predicates/parquet_stats_skipping.rs @@ -1,11 +1,6 @@ //! An implementation of data skipping that leverages parquet stats from the file footer. -use crate::expressions::{ - BinaryExpression, BinaryOperator, ColumnName, Expression as Expr, Scalar, UnaryOperator, - VariadicExpression, VariadicOperator, -}; -use crate::predicates::{ - DataSkippingPredicateEvaluator, PredicateEvaluator, PredicateEvaluatorDefaults, -}; +use crate::expressions::{BinaryOperator, ColumnName, Scalar, VariadicOperator}; +use crate::predicates::{DataSkippingPredicateEvaluator, PredicateEvaluatorDefaults}; use crate::schema::DataType; use std::cmp::Ordering; @@ -65,6 +60,10 @@ impl DataSkippingPredicateEvaluator for T { PredicateEvaluatorDefaults::partial_cmp_scalars(ord, &col, val, inverted) } + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option { + PredicateEvaluatorDefaults::eval_scalar_is_null(val, inverted) + } + fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option { PredicateEvaluatorDefaults::eval_scalar(val, inverted) } @@ -96,109 +95,3 @@ impl DataSkippingPredicateEvaluator for T { PredicateEvaluatorDefaults::finish_eval_variadic(op, exprs, inverted) } } - -/// Data skipping based on parquet footer stats (e.g. row group skipping). The required methods -/// fetch stats values for requested columns (if available and with compatible types), and the -/// provided methods implement the actual skipping logic. -/// -/// NOTE: We are given a row-based filter, but stats-based predicate evaluation -- which applies to -/// a SET of rows -- has different semantics than row-based predicate evaluation. The provided -/// methods of this class convert various supported expressions into data skipping predicates, and -/// then return the result of evaluating the translated filter. -pub(crate) trait ParquetStatsSkippingFilter { - /// Attempts to filter using SQL WHERE semantics. - /// - /// By default, [`apply_expr`] can produce unwelcome behavior for comparisons involving all-NULL - /// columns (e.g. `a == 10`), because the (legitimately NULL) min/max stats are interpreted as - /// stats-missing that produces a NULL data skipping result). The resulting NULL can "poison" - /// the entire expression, causing it to return NULL instead of FALSE that would allow skipping. - /// - /// Meanwhile, SQL WHERE semantics only keep rows for which the filter evaluates to TRUE -- - /// effectively turning `` into the null-safe predicate `AND( IS NOT NULL, )`. - /// - /// We cannot safely evaluate an arbitrary data skipping expression with null-safe semantics - /// (because NULL could also mean missing-stats), but we CAN safely turn a column reference in a - /// comparison into a null-safe comparison, as long as the comparison's parent expressions are - /// all AND. To see why, consider a WHERE clause filter of the form: - /// - /// ```text - /// AND(..., a {cmp} b, ...) - /// ``` - /// - /// In order allow skipping based on the all-null `a` or `b`, we want to actually evaluate: - /// ```text - /// AND(..., AND(a IS NOT NULL, b IS NOT NULL, a {cmp} b), ...) - /// ``` - /// - /// This optimization relies on the fact that we only support IS [NOT] NULL skipping for - /// columns, and we only support skipping for comparisons between columns and literals. Thus, a - /// typical case such as: `AND(..., x < 10, ...)` would in the all-null case be evaluated as: - /// ```text - /// AND(..., AND(x IS NOT NULL, 10 IS NOT NULL, x < 10), ...) - /// AND(..., AND(FALSE, NULL, NULL), ...) - /// AND(..., FALSE, ...) - /// FALSE - /// ``` - /// - /// In the not all-null case, it would instead evaluate as: - /// ```text - /// AND(..., AND(x IS NOT NULL, 10 IS NOT NULL, x < 10), ...) - /// AND(..., AND(TRUE, NULL, ), ...) - /// ``` - /// - /// If the result was FALSE, it forces both inner and outer AND to FALSE, as desired. If the - /// result was TRUE or NULL, then it does not contribute to data skipping but also does not - /// block it if other legs of the AND evaluate to FALSE. - // TODO: If these are generally useful, we may want to move them into PredicateEvaluator? - fn eval_sql_where(&self, filter: &Expr) -> Option; - fn eval_binary_nullsafe(&self, op: BinaryOperator, left: &Expr, right: &Expr) -> Option; -} - -impl> ParquetStatsSkippingFilter for T { - fn eval_sql_where(&self, filter: &Expr) -> Option { - use Expr::{Binary, Variadic}; - match filter { - Variadic(VariadicExpression { - op: VariadicOperator::And, - exprs, - }) => { - let exprs: Vec<_> = exprs - .iter() - .map(|expr| self.eval_sql_where(expr)) - .map(|result| match result { - Some(value) => Expr::literal(value), - None => Expr::null_literal(DataType::BOOLEAN), - }) - .collect(); - self.eval_variadic(VariadicOperator::And, &exprs, false) - } - Binary(BinaryExpression { op, left, right }) => { - self.eval_binary_nullsafe(*op, left, right) - } - _ => self.eval_expr(filter, false), - } - } - - /// Helper method for [`apply_sql_where`], that evaluates `{a} {cmp} {b}` as - /// ```text - /// AND({a} IS NOT NULL, {b} IS NOT NULL, {a} {cmp} {b}) - /// ``` - /// - /// The null checks only apply to column expressions, so at least one of them will always be - /// NULL (since we don't support skipping over column-column comparisons). If any NULL check - /// fails (producing FALSE), it short-circuits the entire AND without ever evaluating the - /// comparison. Otherwise, the original comparison will run and -- if FALSE -- can cause data - /// skipping as usual. - fn eval_binary_nullsafe(&self, op: BinaryOperator, left: &Expr, right: &Expr) -> Option { - use UnaryOperator::IsNull; - // Convert `a {cmp} b` to `AND(a IS NOT NULL, b IS NOT NULL, a {cmp} b)`, - // and only evaluate the comparison if the null checks don't short circuit. - if let Some(false) = self.eval_unary(IsNull, left, true) { - return Some(false); - } - if let Some(false) = self.eval_unary(IsNull, right, true) { - return Some(false); - } - self.eval_binary(op, left, right, false) - } -} diff --git a/kernel/src/predicates/parquet_stats_skipping/tests.rs b/kernel/src/predicates/parquet_stats_skipping/tests.rs index 50833a166..949a4cb68 100644 --- a/kernel/src/predicates/parquet_stats_skipping/tests.rs +++ b/kernel/src/predicates/parquet_stats_skipping/tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::expressions::{column_expr, Expression as Expr}; -use crate::predicates::PredicateEvaluator; +use crate::predicates::PredicateEvaluator as _; use crate::DataType; const TRUE: Option = Some(true); @@ -257,126 +257,3 @@ fn test_eval_is_null() { // all nulls do_test(2, &[TRUE, FALSE]); } - -struct AllNullTestFilter; -impl ParquetStatsProvider for AllNullTestFilter { - fn get_parquet_min_stat(&self, _col: &ColumnName, _data_type: &DataType) -> Option { - None - } - - fn get_parquet_max_stat(&self, _col: &ColumnName, _data_type: &DataType) -> Option { - None - } - - fn get_parquet_nullcount_stat(&self, _col: &ColumnName) -> Option { - Some(self.get_parquet_rowcount_stat()) - } - - fn get_parquet_rowcount_stat(&self) -> i64 { - 10 - } -} - -#[test] -fn test_sql_where() { - let col = &column_expr!("x"); - const VAL: Expr = Expr::Literal(Scalar::Integer(1)); - const NULL: Expr = Expr::Literal(Scalar::Null(DataType::BOOLEAN)); - const FALSE: Expr = Expr::Literal(Scalar::Boolean(false)); - const TRUE: Expr = Expr::Literal(Scalar::Boolean(true)); - - // Basic sanity checks - expect_eq!(AllNullTestFilter.eval_sql_where(&VAL), None, "WHERE {VAL}"); - expect_eq!(AllNullTestFilter.eval_sql_where(col), None, "WHERE {col}"); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::is_null(col.clone())), - Some(true), // No injected NULL checks - "WHERE {col} IS NULL" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::lt(TRUE, FALSE)), - Some(false), // Injected NULL checks don't short circuit when inputs are NOT NULL - "WHERE {TRUE} < {FALSE}" - ); - - // Contrast normal vs SQL WHERE semantics - comparison - expect_eq!( - AllNullTestFilter.eval_expr(&Expr::lt(col.clone(), VAL), false), - None, - "{col} < {VAL}" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::lt(col.clone(), VAL)), - Some(false), - "WHERE {col} < {VAL}" - ); - expect_eq!( - AllNullTestFilter.eval_expr(&Expr::lt(VAL, col.clone()), false), - None, - "{VAL} < {col}" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::lt(VAL, col.clone())), - Some(false), - "WHERE {VAL} < {col}" - ); - - // Contrast normal vs SQL WHERE semantics - comparison inside AND - expect_eq!( - AllNullTestFilter.eval_expr(&Expr::and(NULL, Expr::lt(col.clone(), VAL)), false), - None, - "{NULL} AND {col} < {VAL}" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::and(NULL, Expr::lt(col.clone(), VAL),)), - Some(false), - "WHERE {NULL} AND {col} < {VAL}" - ); - - expect_eq!( - AllNullTestFilter.eval_expr(&Expr::and(TRUE, Expr::lt(col.clone(), VAL)), false), - None, // NULL (from the NULL check) is stronger than TRUE - "{TRUE} AND {col} < {VAL}" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::and(TRUE, Expr::lt(col.clone(), VAL),)), - Some(false), // FALSE (from the NULL check) is stronger than TRUE - "WHERE {TRUE} AND {col} < {VAL}" - ); - - // Contrast normal vs. SQL WHERE semantics - comparison inside AND inside AND - expect_eq!( - AllNullTestFilter.eval_expr( - &Expr::and(TRUE, Expr::and(NULL, Expr::lt(col.clone(), VAL)),), - false, - ), - None, - "{TRUE} AND ({NULL} AND {col} < {VAL})" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::and( - TRUE, - Expr::and(NULL, Expr::lt(col.clone(), VAL)), - )), - Some(false), - "WHERE {TRUE} AND ({NULL} AND {col} < {VAL})" - ); - - // Semantics are the same for comparison inside OR inside AND - expect_eq!( - AllNullTestFilter.eval_expr( - &Expr::or(FALSE, Expr::and(NULL, Expr::lt(col.clone(), VAL)),), - false, - ), - None, - "{FALSE} OR ({NULL} AND {col} < {VAL})" - ); - expect_eq!( - AllNullTestFilter.eval_sql_where(&Expr::or( - FALSE, - Expr::and(NULL, Expr::lt(col.clone(), VAL)), - )), - None, - "WHERE {FALSE} OR ({NULL} AND {col} < {VAL})" - ); -} diff --git a/kernel/src/predicates/tests.rs b/kernel/src/predicates/tests.rs index fa4aec191..fcfb08eb9 100644 --- a/kernel/src/predicates/tests.rs +++ b/kernel/src/predicates/tests.rs @@ -2,7 +2,6 @@ use super::*; use crate::expressions::{ column_expr, column_name, ArrayData, Expression, StructData, UnaryOperator, }; -use crate::predicates::PredicateEvaluator; use crate::schema::ArrayType; use crate::DataType; @@ -394,12 +393,12 @@ fn test_eval_is_null() { let expr = Expression::literal(1); expect_eq!( filter.eval_unary(UnaryOperator::IsNull, &expr, true), - None, + Some(true), "1 IS NOT NULL" ); expect_eq!( filter.eval_unary(UnaryOperator::IsNull, &expr, false), - None, + Some(false), "1 IS NULL" ); } @@ -570,3 +569,81 @@ fn eval_binary() { ); } } + +// NOTE: `None` is NOT equivalent to `Some(Scalar::Null)` +struct NullColumnResolver; +impl ResolveColumnAsScalar for NullColumnResolver { + fn resolve_column(&self, _col: &ColumnName) -> Option { + Some(Scalar::Null(DataType::INTEGER)) + } +} + +#[test] +fn test_sql_where() { + let col = &column_expr!("x"); + const VAL: Expr = Expr::Literal(Scalar::Integer(1)); + const NULL: Expr = Expr::Literal(Scalar::Null(DataType::BOOLEAN)); + const FALSE: Expr = Expr::Literal(Scalar::Boolean(false)); + const TRUE: Expr = Expr::Literal(Scalar::Boolean(true)); + let null_filter = DefaultPredicateEvaluator::from(NullColumnResolver); + let empty_filter = DefaultPredicateEvaluator::from(EmptyColumnResolver); + + // Basic sanity checks + expect_eq!(null_filter.eval_sql_where(&VAL), None, "WHERE {VAL}"); + expect_eq!(null_filter.eval_sql_where(col), None, "WHERE {col}"); + + // SQL eval does not modify behavior of IS NULL + let expr = &Expr::is_null(col.clone()); + expect_eq!(null_filter.eval_sql_where(expr), Some(true), "{expr}"); + + // Injected NULL checks only short circuit if inputs are NULL + let expr = &Expr::lt(FALSE, TRUE); + expect_eq!(null_filter.eval_sql_where(expr), Some(true), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), Some(true), "{expr}"); + + // Constrast normal vs SQL WHERE semantics - comparison + let expr = &Expr::lt(col.clone(), VAL); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + // NULL check produces NULL due to missing column + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + let expr = &Expr::lt(VAL, col.clone()); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + let expr = &Expr::distinct(VAL, col.clone()); + expect_eq!(null_filter.eval_expr(expr, false), Some(true), "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(true), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + let expr = &Expr::distinct(NULL, col.clone()); + expect_eq!(null_filter.eval_expr(expr, false), Some(false), "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + // Constrast normal vs SQL WHERE semantics - comparison inside AND + let expr = &Expr::and(NULL, Expr::lt(col.clone(), VAL)); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + // NULL/FALSE (from the NULL check) is stronger than TRUE + let expr = &Expr::and(TRUE, Expr::lt(col.clone(), VAL)); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + // Contrast normal vs. SQL WHERE semantics - comparison inside AND inside AND + let expr = &Expr::and(TRUE, Expr::and(NULL, Expr::lt(col.clone(), VAL))); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); + + // Ditto for comparison inside OR inside AND + let expr = &Expr::or(FALSE, Expr::and(NULL, Expr::lt(col.clone(), VAL))); + expect_eq!(null_filter.eval_expr(expr, false), None, "{expr}"); + expect_eq!(null_filter.eval_sql_where(expr), Some(false), "{expr}"); + expect_eq!(empty_filter.eval_sql_where(expr), None, "{expr}"); +} diff --git a/kernel/src/scan/data_skipping.rs b/kernel/src/scan/data_skipping.rs index b30711f48..cea54c4c9 100644 --- a/kernel/src/scan/data_skipping.rs +++ b/kernel/src/scan/data_skipping.rs @@ -36,8 +36,15 @@ mod tests; /// are not eligible for data skipping. /// - `OR` is rewritten only if all operands are eligible for data skipping. Otherwise, the whole OR /// expression is dropped. -fn as_data_skipping_predicate(expr: &Expr, inverted: bool) -> Option { - DataSkippingPredicateCreator.eval_expr(expr, inverted) +#[cfg(test)] +fn as_data_skipping_predicate(expr: &Expr) -> Option { + DataSkippingPredicateCreator.eval_expr(expr, false) +} + +/// Like `as_data_skipping_predicate`, but invokes [`PredicateEvaluator::eval_sql_where`] instead +/// of [`PredicateEvaluator::eval_expr`]. +fn as_sql_data_skipping_predicate(expr: &Expr) -> Option { + DataSkippingPredicateCreator.eval_sql_where(expr) } pub(crate) struct DataSkippingFilter { @@ -108,7 +115,7 @@ impl DataSkippingFilter { let skipping_evaluator = engine.get_expression_handler().get_evaluator( stats_schema.clone(), - Expr::struct_from([as_data_skipping_predicate(&predicate, false)?]), + Expr::struct_from([as_sql_data_skipping_predicate(&predicate)?]), PREDICATE_SCHEMA.clone(), ); @@ -205,6 +212,10 @@ impl DataSkippingPredicateEvaluator for DataSkippingPredicateCreator { Some(Expr::binary(op, col, val.clone())) } + fn eval_scalar_is_null(&self, val: &Scalar, inverted: bool) -> Option { + PredicateEvaluatorDefaults::eval_scalar_is_null(val, inverted).map(Expr::literal) + } + fn eval_scalar(&self, val: &Scalar, inverted: bool) -> Option { PredicateEvaluatorDefaults::eval_scalar(val, inverted).map(Expr::literal) } diff --git a/kernel/src/scan/data_skipping/tests.rs b/kernel/src/scan/data_skipping/tests.rs index e12adb526..4f1d74f63 100644 --- a/kernel/src/scan/data_skipping/tests.rs +++ b/kernel/src/scan/data_skipping/tests.rs @@ -34,7 +34,7 @@ fn test_eval_is_null() { ]); let filter = DefaultPredicateEvaluator::from(resolver); for (expr, expect) in expressions.iter().zip(expected) { - let pred = as_data_skipping_predicate(expr, false).unwrap(); + let pred = as_data_skipping_predicate(expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), *expect, @@ -77,7 +77,7 @@ fn test_eval_binary_comparisons() { ]); let filter = DefaultPredicateEvaluator::from(resolver); for (expr, expect) in expressions.iter().zip(expected.iter()) { - let pred = as_data_skipping_predicate(expr, false).unwrap(); + let pred = as_data_skipping_predicate(expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), *expect, @@ -160,7 +160,7 @@ fn test_eval_variadic() { .collect(); let expr = Expr::and_from(inputs.clone()); - let pred = as_data_skipping_predicate(&expr, false).unwrap(); + let pred = as_data_skipping_predicate(&expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), *expect_and, @@ -168,19 +168,19 @@ fn test_eval_variadic() { ); let expr = Expr::or_from(inputs.clone()); - let pred = as_data_skipping_predicate(&expr, false).unwrap(); + let pred = as_data_skipping_predicate(&expr).unwrap(); expect_eq!(filter.eval_expr(&pred, false), *expect_or, "OR({inputs:?})"); - let expr = Expr::and_from(inputs.clone()); - let pred = as_data_skipping_predicate(&expr, true).unwrap(); + let expr = !Expr::and_from(inputs.clone()); + let pred = as_data_skipping_predicate(&expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), expect_and.map(|val| !val), "NOT AND({inputs:?})" ); - let expr = Expr::or_from(inputs.clone()); - let pred = as_data_skipping_predicate(&expr, true).unwrap(); + let expr = !Expr::or_from(inputs.clone()); + let pred = as_data_skipping_predicate(&expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), expect_or.map(|val| !val), @@ -216,7 +216,7 @@ fn test_eval_distinct() { ]); let filter = DefaultPredicateEvaluator::from(resolver); for (expr, expect) in expressions.iter().zip(expected) { - let pred = as_data_skipping_predicate(expr, false).unwrap(); + let pred = as_data_skipping_predicate(expr).unwrap(); expect_eq!( filter.eval_expr(&pred, false), *expect, @@ -252,3 +252,93 @@ fn test_eval_distinct() { // min < value < max, all nulls do_test(five, fifteen, 2, &[TRUE, FALSE, FALSE, TRUE]); } + +#[test] +fn test_sql_where() { + let col = &column_expr!("x"); + const VAL: Expr = Expr::Literal(Scalar::Integer(10)); + const NULL: Expr = Expr::Literal(Scalar::Null(DataType::BOOLEAN)); + const FALSE: Expr = Expr::Literal(Scalar::Boolean(false)); + const TRUE: Expr = Expr::Literal(Scalar::Boolean(true)); + + const ROWCOUNT: i64 = 2; + const ALL_NULL: i64 = ROWCOUNT; + const SOME_NULL: i64 = 1; + const NO_NULL: i64 = 0; + let do_test = + |nulls: i64, expr: &Expr, missing: bool, expect: Option, expect_sql: Option| { + assert!((0..=ROWCOUNT).contains(&nulls)); + let (min, max) = if nulls < ROWCOUNT { + (Scalar::Integer(5), Scalar::Integer(15)) + } else { + ( + Scalar::Null(DataType::INTEGER), + Scalar::Null(DataType::INTEGER), + ) + }; + let resolver = if missing { + HashMap::new() + } else { + HashMap::from_iter([ + (column_name!("numRecords"), Scalar::from(ROWCOUNT)), + (column_name!("nullCount.x"), Scalar::from(nulls)), + (column_name!("minValues.x"), min.clone()), + (column_name!("maxValues.x"), max.clone()), + ]) + }; + let filter = DefaultPredicateEvaluator::from(resolver); + let pred = as_data_skipping_predicate(expr).unwrap(); + expect_eq!( + filter.eval_expr(&pred, false), + expect, + "{expr:#?} became {pred:#?} ({min}..{max}, {nulls} nulls)" + ); + let sql_pred = as_sql_data_skipping_predicate(expr).unwrap(); + expect_eq!( + filter.eval_expr(&sql_pred, false), + expect_sql, + "{expr:#?} became {sql_pred:#?} ({min}..{max}, {nulls} nulls)" + ); + }; + + // Sanity tests -- only all-null columns should behave differently between normal and SQL WHERE. + const MISSING: bool = true; + const PRESENT: bool = false; + let expr = &Expr::lt(TRUE, FALSE); + do_test(ALL_NULL, expr, MISSING, Some(false), Some(false)); + + let expr = &Expr::is_not_null(col.clone()); + do_test(ALL_NULL, expr, PRESENT, Some(false), Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + // SQL WHERE allows a present-but-all-null column to be pruned, but not a missing column. + let expr = &Expr::lt(col.clone(), VAL); + do_test(NO_NULL, expr, PRESENT, Some(true), Some(true)); + do_test(SOME_NULL, expr, PRESENT, Some(true), Some(true)); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + // Comparison inside AND works + let expr = &Expr::and(NULL, Expr::lt(col.clone(), VAL)); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + let expr = &Expr::and(TRUE, Expr::lt(VAL, col.clone())); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + // Comparison inside AND inside AND works + let expr = &Expr::and(TRUE, Expr::and(NULL, Expr::lt(col.clone(), VAL))); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + // Comparison inside OR works + let expr = &Expr::or(FALSE, Expr::lt(col.clone(), VAL)); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); + + // Comparison inside AND inside OR works + let expr = &Expr::or(FALSE, Expr::and(TRUE, Expr::lt(col.clone(), VAL))); + do_test(ALL_NULL, expr, PRESENT, None, Some(false)); + do_test(ALL_NULL, expr, MISSING, None, None); +} diff --git a/kernel/src/scan/mod.rs b/kernel/src/scan/mod.rs index cd17bca7d..49af0222c 100644 --- a/kernel/src/scan/mod.rs +++ b/kernel/src/scan/mod.rs @@ -13,9 +13,7 @@ use crate::actions::deletion_vector::{ }; use crate::actions::{get_log_add_schema, get_log_schema, ADD_NAME, REMOVE_NAME}; use crate::expressions::{ColumnName, Expression, ExpressionRef, ExpressionTransform, Scalar}; -use crate::predicates::parquet_stats_skipping::{ - ParquetStatsProvider, ParquetStatsSkippingFilter as _, -}; +use crate::predicates::{DefaultPredicateEvaluator, EmptyColumnResolver}; use crate::scan::state::{DvInfo, Stats}; use crate::schema::{ ArrayType, DataType, MapType, PrimitiveType, Schema, SchemaRef, SchemaTransform, StructField, @@ -184,27 +182,10 @@ impl PhysicalPredicate { // Evaluates a static data skipping predicate, ignoring any column references, and returns true if // the predicate allows to statically skip all files. Since this is direct evaluation (not an -// expression rewrite), we use a dummy `ParquetStatsProvider` that provides no stats. +// expression rewrite), we use a `DefaultPredicateEvaluator` with an empty column resolver. fn can_statically_skip_all_files(predicate: &Expression) -> bool { - struct NoStats; - impl ParquetStatsProvider for NoStats { - fn get_parquet_min_stat(&self, _: &ColumnName, _: &DataType) -> Option { - None - } - - fn get_parquet_max_stat(&self, _: &ColumnName, _: &DataType) -> Option { - None - } - - fn get_parquet_nullcount_stat(&self, _: &ColumnName) -> Option { - None - } - - fn get_parquet_rowcount_stat(&self) -> i64 { - 0 - } - } - NoStats.eval_sql_where(predicate) == Some(false) + use crate::predicates::PredicateEvaluator as _; + DefaultPredicateEvaluator::from(EmptyColumnResolver).eval_sql_where(predicate) == Some(false) } // Build the stats read schema filtering the table schema to keep only skipping-eligible