buoyant_kernel/lib.rs
1//! # Delta Kernel
2//!
3//! Delta-kernel-rs is an experimental [Delta](https://github.com/delta-io/delta/) implementation
4//! focused on interoperability with a wide range of query engines. It supports reads and
5//! (experimental) writes (only blind appends in the write path currently). This library defines a
6//! number of traits which must be implemented to provide a working delta implementation. They are
7//! detailed below. There is a provided "default engine" that implements all these traits and can
8//! be used to ease integration work. See [`DefaultEngine`](engine/default/index.html) for more
9//! information.
10//!
11//! A full `rust` example for reading table data using the default engine can be found in the
12//! [read-table-single-threaded] example (and for a more complex multi-threaded reader see the
13//! [read-table-multi-threaded] example). An example for reading the table changes for a table
14//! using the default engine can be found in the [read-table-changes] example. The [write-table]
15//! example demonstrates how to write data to a Delta table using the default engine.
16//!
17//! [read-table-single-threaded]:
18//! https://github.com/delta-io/delta-kernel-rs/tree/main/kernel/examples/read-table-single-threaded
19//! [read-table-multi-threaded]:
20//! https://github.com/delta-io/delta-kernel-rs/tree/main/kernel/examples/read-table-multi-threaded
21//! [read-table-changes]:
22//! https://github.com/delta-io/delta-kernel-rs/tree/main/kernel/examples/read-table-changes
23//! [write-table]:
24//! https://github.com/delta-io/delta-kernel-rs/tree/main/kernel/examples/write-table
25//!
26//! # Engine trait
27//!
28//! The [`Engine`] trait allows connectors to bring their own implementation of functionality such
29//! as reading parquet files, listing files in a file system, parsing a JSON string etc. This
30//! trait exposes methods to get sub-engines which expose the core functionalities customizable by
31//! connectors.
32//!
33//! ## Expression handling
34//!
35//! Expression handling is done via the [`EvaluationHandler`], which in turn allows the creation of
36//! [`ExpressionEvaluator`]s. These evaluators are created for a specific predicate [`Expression`]
37//! and allow evaluation of that predicate for a specific batch of data.
38//!
39//! ## File system interactions
40//!
41//! Delta Kernel needs to perform some basic operations against file systems like listing and
42//! reading files. These interactions are encapsulated in the [`StorageHandler`] trait.
43//! Implementers must take care that all assumptions on the behavior of the functions - like sorted
44//! results - are respected.
45//!
46//! ## Reading log and data files
47//!
48//! Delta Kernel requires the capability to read and write json files and read parquet files, which
49//! is exposed via the [`JsonHandler`] and [`ParquetHandler`] respectively. When reading files,
50//! connectors are asked to provide the context information they require to execute the actual
51//! operation. This is done by invoking methods on the [`StorageHandler`] trait.
52
53#![cfg_attr(all(doc, NIGHTLY_CHANNEL), feature(doc_cfg))]
54#![warn(
55 unreachable_pub,
56 trivial_numeric_casts,
57 unused_extern_crates,
58 rust_2018_idioms,
59 rust_2021_compatibility,
60 clippy::unwrap_used,
61 clippy::expect_used,
62 clippy::panic
63)]
64// we re-allow panics in tests
65#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
66
67/// This `extern crate` declaration allows the macro to reliably refer to
68/// `delta_kernel::schema::DataType` no matter which crate invokes it. Without that, `delta_kernel`
69/// cannot invoke the macro because `delta_kernel` is an unknown crate identifier (you have to use
70/// `crate` instead). We could make the macro use `crate::schema::DataType` instead, but then the
71/// macro is useless outside the `delta_kernel` crate.
72// TODO: when running `cargo package -p delta_kernel` this gives 'unused' warning - #1095
73#[allow(unused_extern_crates)]
74extern crate self as delta_kernel;
75
76use std::any::Any;
77use std::cmp::Ordering;
78use std::fs::DirEntry;
79use std::ops::Range;
80use std::sync::Arc;
81use std::time::SystemTime;
82
83use bytes::Bytes;
84use url::Url;
85
86use self::schema::{DataType, SchemaRef};
87
88mod action_reconciliation;
89pub mod actions;
90pub mod checkpoint;
91pub mod commit_range;
92pub mod committer;
93#[cfg(feature = "internal-api")]
94pub mod crc;
95#[cfg(not(feature = "internal-api"))]
96pub(crate) mod crc;
97pub mod engine_data;
98pub mod error;
99pub mod expressions;
100pub mod incremental_scan;
101mod log_compaction;
102mod log_path;
103mod log_reader;
104pub mod metrics;
105pub mod partition;
106#[cfg(feature = "declarative-plans")]
107pub mod plans;
108pub mod scan;
109pub mod schema;
110pub mod snapshot;
111pub mod struct_patch;
112pub mod table_changes;
113pub mod table_configuration;
114pub mod table_features;
115pub mod table_properties;
116pub mod transaction;
117pub mod transforms;
118
119pub use crc::{FileSizeHistogram, FileStats};
120pub use log_path::LogPath;
121
122// Public under test-utils so integration tests can call get_high_water_mark via snapshot.
123#[cfg(feature = "test-utils")]
124pub mod row_tracking;
125#[cfg(not(feature = "test-utils"))]
126pub(crate) mod row_tracking;
127
128pub(crate) mod clustering;
129
130mod arrow_compat;
131#[cfg(any(feature = "arrow-58", feature = "arrow-59"))]
132pub use arrow_compat::*;
133
134#[cfg(feature = "internal-api")]
135pub mod column_trie;
136#[cfg(not(feature = "internal-api"))]
137pub(crate) mod column_trie;
138pub mod kernel_predicates;
139#[cfg(feature = "internal-api")]
140pub mod utils;
141#[cfg(not(feature = "internal-api"))]
142pub(crate) mod utils;
143
144#[cfg(feature = "internal-api")]
145pub use utils::{try_parse_uri, CollectInto};
146
147// for the below modules, we cannot introduce a macro to clean this up. rustfmt doesn't follow into
148// macros, and so will not format the files associated with these modules if we get too clever. see:
149// https://github.com/rust-lang/rustfmt/issues/3253
150
151#[cfg(feature = "internal-api")]
152pub mod path;
153#[cfg(not(feature = "internal-api"))]
154pub(crate) mod path;
155
156#[cfg(feature = "internal-api")]
157pub mod log_replay;
158#[cfg(not(feature = "internal-api"))]
159pub(crate) mod log_replay;
160
161#[cfg(feature = "internal-api")]
162pub mod log_segment;
163#[cfg(not(feature = "internal-api"))]
164pub(crate) mod log_segment;
165
166#[cfg(feature = "internal-api")]
167pub mod last_checkpoint_hint;
168#[cfg(not(feature = "internal-api"))]
169pub(crate) mod last_checkpoint_hint;
170
171pub(crate) mod log_segment_files;
172
173pub mod history_manager;
174
175#[cfg(feature = "internal-api")]
176pub mod parallel;
177#[cfg(not(feature = "internal-api"))]
178pub(crate) mod parallel;
179
180pub use action_reconciliation::{ActionReconciliationIterator, ActionReconciliationIteratorState};
181pub use delta_kernel_derive;
182use delta_kernel_derive::internal_api;
183pub use engine_data::{
184 EngineData, FilteredEngineData, FilteredRowVisitor, GetData, RowIndexIterator, RowVisitor,
185};
186pub use error::{DeltaResult, DeltaResultIterator, DeltaResultIteratorStatic, Error};
187use expressions::{literal_expression_transform, Scalar};
188pub use expressions::{Expression, ExpressionRef, Predicate, PredicateRef};
189pub use log_compaction::{should_compact, LogCompactionWriter};
190#[cfg(feature = "declarative-plans")]
191pub use plans::{IoOperation, Operation, PlanBuilder, PlanExecutor, PlanResult};
192use schema::{StructField, StructType};
193pub use snapshot::{Snapshot, SnapshotRef};
194
195#[cfg(any(
196 feature = "default-engine-base",
197 feature = "arrow-conversion",
198 feature = "declarative-plans"
199))]
200pub mod engine;
201
202/// Delta table version is 8 byte unsigned int
203pub type Version = u64;
204
205pub type FileSize = u64;
206pub type FileIndex = u64;
207
208/// A specification for a range of bytes to read from a file location
209pub type FileSlice = (Url, Option<Range<FileIndex>>);
210
211/// Data read from a Delta table file and the corresponding scan file information.
212pub type FileDataReadResult = (FileMeta, Box<dyn EngineData>);
213
214/// An iterator of data read from specified files
215pub type FileDataReadResultIterator = DeltaResultIteratorStatic<Box<dyn EngineData>>;
216
217/// The metadata that describes an object.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct FileMeta {
220 /// The fully qualified path to the object
221 pub location: Url,
222 /// The last modified time as milliseconds since unix epoch
223 pub last_modified: i64,
224 /// The size in bytes of the object
225 pub size: FileSize,
226}
227
228impl Ord for FileMeta {
229 fn cmp(&self, other: &Self) -> Ordering {
230 self.location.cmp(&other.location)
231 }
232}
233
234impl PartialOrd for FileMeta {
235 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
236 Some(self.cmp(other))
237 }
238}
239
240impl TryFrom<DirEntry> for FileMeta {
241 type Error = Error;
242
243 fn try_from(ent: DirEntry) -> DeltaResult<FileMeta> {
244 let metadata = ent.metadata()?;
245 let last_modified = metadata
246 .modified()?
247 .duration_since(SystemTime::UNIX_EPOCH)
248 .map_err(|_| Error::generic("Failed to convert file timestamp to milliseconds"))?;
249 let location = Url::from_file_path(ent.path())
250 .map_err(|_| Error::generic(format!("Invalid path: {:?}", ent.path())))?;
251 let last_modified = last_modified.as_millis().try_into().map_err(|_| {
252 Error::generic(format!(
253 "Failed to convert file modification time {:?} into i64",
254 last_modified.as_millis()
255 ))
256 })?;
257 Ok(FileMeta {
258 location,
259 last_modified,
260 size: metadata.len(),
261 })
262 }
263}
264
265impl FileMeta {
266 /// Create a new instance of `FileMeta`
267 pub fn new(location: Url, last_modified: i64, size: u64) -> Self {
268 Self {
269 location,
270 last_modified,
271 size,
272 }
273 }
274
275 /// Casts `size` to `i64`. Errors if `size` exceeds `i64::MAX`.
276 pub(crate) fn size_as_i64(&self) -> DeltaResult<i64> {
277 i64::try_from(self.size)
278 .map_err(|_| Error::generic(format!("file size {} exceeds i64::MAX", self.size)))
279 }
280}
281
282/// Extension trait that makes it easier to work with traits objects that implement [`Any`],
283/// implemented automatically for any type that satisfies `Any`, `Send`, and `Sync`. In particular,
284/// given some `trait T: Any + Send + Sync`, it allows upcasting `T` to `dyn Any + Send + Sync`,
285/// which in turn allows downcasting the result to a concrete type.
286///
287/// For example, the following code will compile:
288///
289/// ```
290/// # use buoyant_kernel as delta_kernel;
291/// # use delta_kernel::AsAny;
292/// # use std::any::Any;
293/// # use std::sync::Arc;
294/// trait Foo : AsAny {}
295/// struct Bar;
296/// impl Foo for Bar {}
297///
298/// let f: Arc<dyn Foo> = Arc::new(Bar);
299/// let a: Arc<dyn Any + Send + Sync> = f.as_any();
300/// let b: Arc<Bar> = a.downcast().unwrap();
301/// ```
302///
303/// In contrast, very similar code that relies only on `Any` would fail to compile:
304///
305/// ```fail_compile
306/// # use std::any::Any;
307/// # use std::sync::Arc;
308/// trait Foo: Any + Send + Sync {}
309///
310/// struct Bar;
311/// impl Foo for Bar {}
312///
313/// let f: Arc<dyn Foo> = Arc::new(Bar);
314/// let b: Arc<Bar> = f.downcast().unwrap(); // `Arc::downcast` method not found
315/// ```
316///
317/// As would this:
318///
319/// ```fail_compile
320/// # use std::any::Any;
321/// # use std::sync::Arc;
322/// trait Foo: Any + Send + Sync {}
323///
324/// struct Bar;
325/// impl Foo for Bar {}
326///
327/// let f: Arc<dyn Foo> = Arc::new(Bar);
328/// let a: Arc<dyn Any + Send + Sync> = f; // trait upcasting coercion is not stable rust
329/// let f: Arc<Bar> = a.downcast().unwrap();
330/// ```
331///
332/// NOTE: `AsAny` inherits the `Send + Sync` constraint from [`Arc::downcast`].
333pub trait AsAny: Any + Send + Sync {
334 /// Obtains a `dyn Any` reference to the object:
335 ///
336 /// ```
337 /// # use buoyant_kernel as delta_kernel;
338 /// # use delta_kernel::AsAny;
339 /// # use std::any::Any;
340 /// # use std::sync::Arc;
341 /// trait Foo : AsAny {}
342 /// struct Bar;
343 /// impl Foo for Bar {}
344 ///
345 /// let f: &dyn Foo = &Bar;
346 /// let a: &dyn Any = f.any_ref();
347 /// let b: &Bar = a.downcast_ref().unwrap();
348 /// ```
349 fn any_ref(&self) -> &(dyn Any + Send + Sync);
350
351 /// Obtains an `Arc<dyn Any>` reference to the object:
352 ///
353 /// ```
354 /// # use buoyant_kernel as delta_kernel;
355 /// # use delta_kernel::AsAny;
356 /// # use std::any::Any;
357 /// # use std::sync::Arc;
358 /// trait Foo : AsAny {}
359 /// struct Bar;
360 /// impl Foo for Bar {}
361 ///
362 /// let f: Arc<dyn Foo> = Arc::new(Bar);
363 /// let a: Arc<dyn Any + Send + Sync> = f.as_any();
364 /// let b: Arc<Bar> = a.downcast().unwrap();
365 /// ```
366 fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
367
368 /// Converts the object to `Box<dyn Any>`:
369 ///
370 /// ```
371 /// # use buoyant_kernel as delta_kernel;
372 /// # use delta_kernel::AsAny;
373 /// # use std::any::Any;
374 /// # use std::sync::Arc;
375 /// trait Foo : AsAny {}
376 /// struct Bar;
377 /// impl Foo for Bar {}
378 ///
379 /// let f: Box<dyn Foo> = Box::new(Bar);
380 /// let a: Box<dyn Any> = f.into_any();
381 /// let b: Box<Bar> = a.downcast().unwrap();
382 /// ```
383 fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
384
385 /// Convenient wrapper for [`std::any::type_name`], since [`Any`] does not provide it and
386 /// [`Any::type_id`] is useless as a debugging aid (its `Debug` is just a mess of hex digits).
387 fn type_name(&self) -> &'static str;
388}
389
390// Blanket implementation for all eligible types
391impl<T: Any + Send + Sync> AsAny for T {
392 fn any_ref(&self) -> &(dyn Any + Send + Sync) {
393 self
394 }
395 fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
396 self
397 }
398 fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync> {
399 self
400 }
401 fn type_name(&self) -> &'static str {
402 std::any::type_name::<Self>()
403 }
404}
405
406/// Extension trait that facilitates object-safe implementations of `PartialEq`.
407pub trait DynPartialEq: AsAny {
408 fn dyn_eq(&self, other: &dyn Any) -> bool;
409}
410
411// Blanket implementation for all eligible types
412impl<T: PartialEq + AsAny> DynPartialEq for T {
413 fn dyn_eq(&self, other: &dyn Any) -> bool {
414 other.downcast_ref::<T>().is_some_and(|other| self == other)
415 }
416}
417
418/// Trait for implementing an Expression evaluator.
419///
420/// It contains one Expression which can be evaluated on multiple ColumnarBatches.
421/// Connectors can implement this trait to optimize the evaluation using the
422/// connector specific capabilities.
423pub trait ExpressionEvaluator: AsAny {
424 /// Evaluate the expression on a given EngineData.
425 ///
426 /// Produces one value for each row of the input.
427 /// The data type of the output is same as the type output of the expression this evaluator is
428 /// using.
429 fn evaluate(&self, batch: &dyn EngineData) -> DeltaResult<Box<dyn EngineData>>;
430}
431
432/// Trait for implementing a Predicate evaluator.
433///
434/// It contains one Predicate which can be evaluated on multiple ColumnarBatches.
435/// Connectors can implement this trait to optimize the evaluation using the
436/// connector specific capabilities.
437pub trait PredicateEvaluator: AsAny {
438 /// Evaluate the predicate on a given EngineData.
439 ///
440 /// Produces one boolean value for each row of the input.
441 fn evaluate(&self, batch: &dyn EngineData) -> DeltaResult<Box<dyn EngineData>>;
442}
443
444/// Provides expression evaluation capability to Delta Kernel.
445///
446/// Delta Kernel can use this handler to evaluate a predicate on partition filters,
447/// fill up partition column values, and any computation on data using Expressions.
448pub trait EvaluationHandler: AsAny {
449 /// Create an [`ExpressionEvaluator`] that can evaluate the given [`Expression`]
450 /// on columnar batches with the given [`Schema`] to produce data of [`DataType`].
451 ///
452 /// If the provided output type is a struct, its fields describe the columns of output produced
453 /// by the evaluator. Otherwise, the output schema is a single column named "output" of the
454 /// specified `output_type`. In all cases, the output schema is only used for its names (all
455 /// field names will be updated to match) and nullability (non-nullable columns can be converted
456 /// to nullable). Any mismatch in types (including number of columns) will produce an error.
457 ///
458 /// # Parameters
459 ///
460 /// - `input_schema`: Schema of the input data.
461 /// - `expression`: Expression to evaluate.
462 /// - `output_type`: Expected result data type.
463 ///
464 /// [`Schema`]: crate::schema::StructType
465 /// [`DataType`]: crate::schema::DataType
466 fn new_expression_evaluator(
467 &self,
468 input_schema: SchemaRef,
469 expression: ExpressionRef,
470 output_type: DataType,
471 ) -> DeltaResult<Arc<dyn ExpressionEvaluator>>;
472
473 /// Create a [`PredicateEvaluator`] that can evaluate the given [`Predicate`] on columnar
474 /// batches with the given [`Schema`] to produce a column of boolean results.
475 ///
476 /// The output schema is a single nullable boolean column named "output".
477 ///
478 /// # Parameters
479 ///
480 /// - `input_schema`: Schema of the input data.
481 /// - `predicate`: Predicate to evaluate.
482 ///
483 /// [`Schema`]: crate::schema::StructType
484 fn new_predicate_evaluator(
485 &self,
486 input_schema: SchemaRef,
487 predicate: PredicateRef,
488 ) -> DeltaResult<Arc<dyn PredicateEvaluator>>;
489
490 /// Create a single-row all-null-value [`EngineData`] with the schema specified by
491 /// `output_schema`.
492 // NOTE: we should probably allow DataType instead of SchemaRef, but can expand that in the
493 // future.
494 fn null_row(&self, output_schema: SchemaRef) -> DeltaResult<Box<dyn EngineData>>;
495
496 /// Create a multi-row [`EngineData`] by applying the given schema to multiple rows of values.
497 ///
498 /// Each element in `rows` represents one row of data, where each row is a slice of structured
499 /// scalar values (one scalar per top-level field in the schema).
500 ///
501 /// # Parameters
502 ///
503 /// - `schema`: Schema describing the structure of each row.
504 /// - `rows`: Slice of rows, where each row contains one structured scalar per top-level schema
505 /// field.
506 ///
507 /// # Returns
508 ///
509 /// A multi-row `EngineData` containing all rows.
510 ///
511 /// # Errors
512 ///
513 /// Returns an error if any row has a number of scalars that does not match the number of
514 /// top-level fields in `schema`, or if any scalar value cannot be appended to its corresponding
515 /// field's builder (e.g. due to a type mismatch).
516 ///
517 /// # Example
518 ///
519 /// For a schema with fields `[add: Struct, remove: Struct]`, each row should contain exactly 2
520 /// scalars: one for the `add` field and one for the `remove` field.
521 fn create_many(
522 &self,
523 schema: SchemaRef,
524 rows: &[&[Scalar]],
525 ) -> DeltaResult<Box<dyn EngineData>>;
526}
527
528/// Internal trait to allow us to have a private `create_one` API that's implemented for all
529/// EvaluationHandlers.
530// For some reason rustc doesn't detect it's usage so we allow(dead_code) here...
531#[allow(dead_code)]
532#[internal_api]
533trait EvaluationHandlerExtension: EvaluationHandler {
534 /// Create a single-row [`EngineData`] by applying the given schema to the leaf-values given in
535 /// `values`.
536 // Note: we will stick with a Schema instead of DataType (more constrained can expand in
537 // future)
538 fn create_one(&self, schema: SchemaRef, values: &[Scalar]) -> DeltaResult<Box<dyn EngineData>> {
539 // just get a single int column (arbitrary)
540 let null_row_schema = Arc::new(StructType::new_unchecked(vec![StructField::nullable(
541 "null_col",
542 DataType::INTEGER,
543 )]));
544 let null_row = self.null_row(null_row_schema.clone())?;
545
546 // Convert schema and leaf values to an expression
547 let row_expr = literal_expression_transform(schema.as_ref(), values)?;
548
549 let eval =
550 self.new_expression_evaluator(null_row_schema, row_expr.into(), schema.into())?;
551 eval.evaluate(null_row.as_ref())
552 }
553}
554
555// Auto-implement the extension trait for all EvaluationHandlers
556impl<T: EvaluationHandler + ?Sized> EvaluationHandlerExtension for T {}
557
558/// A trait that allows converting a type into (single-row) EngineData
559///
560/// This is typically used with the `#[derive(IntoEngineData)]` macro
561/// which leverages the traits `ToDataType` and `Into<Scalar>` for struct fields
562/// to convert a struct into EngineData.
563///
564/// # Example
565/// ```ignore
566/// # use buoyant_kernel as delta_kernel;
567/// # use std::sync::Arc;
568/// # use delta_kernel_derive::{Schema, IntoEngineData};
569///
570/// #[derive(Schema, IntoEngineData)]
571/// struct MyStruct {
572/// a: i32,
573/// b: String,
574/// }
575///
576/// let my_struct = MyStruct { a: 42, b: "Hello".to_string() };
577/// // typically used with ToSchema
578/// let schema = Arc::new(MyStruct::to_schema());
579/// // single-row EngineData
580/// let engine = todo!(); // create an engine
581/// let engine_data = my_struct.into_engine_data(schema, engine);
582/// ```
583#[internal_api]
584pub(crate) trait IntoEngineData {
585 /// Consume this type to produce a single-row EngineData using the provided schema.
586 fn into_engine_data(
587 self,
588 schema: SchemaRef,
589 engine: &dyn Engine,
590 ) -> DeltaResult<Box<dyn EngineData>>;
591}
592
593/// Provides file system related functionalities to Delta Kernel.
594///
595/// Delta Kernel uses this handler whenever it needs to access the underlying
596/// file system where the Delta table is present. Connector implementation of
597/// this trait can hide filesystem specific details from Delta Kernel.
598pub trait StorageHandler: AsAny {
599 /// Recursively list files whose full path is lexicographically greater than (UTF-8 sorting)
600 /// the given `path`, restricted to descendants of `path`'s parent directory. The result must
601 /// be sorted by the full path (UTF-8 byte order).
602 ///
603 /// The listing is **recursive**: files in nested subdirectories are included, not just files
604 /// directly under the parent. For example, listing from `dir/0001.json` may return
605 /// `dir/0002.json`, `dir/sub/0003.json`, and `dir/sub/nested/0004.json`, all interleaved
606 /// in lexicographic order.
607 ///
608 /// The parent directory is derived from `path`:
609 /// - If `path` is directory-like (ends with `/`), the parent is `path` itself and the result
610 /// contains all files at or below that directory.
611 /// - Otherwise, the parent is the directory containing `path`, and only files (at any depth
612 /// under that parent) whose full path sorts strictly greater than `path` are returned.
613 fn list_from(&self, path: &Url)
614 -> DeltaResult<Box<dyn Iterator<Item = DeltaResult<FileMeta>>>>;
615
616 /// Read data specified by the start and end offset from the file.
617 fn read_files(
618 &self,
619 files: Vec<FileSlice>,
620 ) -> DeltaResult<Box<dyn Iterator<Item = DeltaResult<Bytes>>>>;
621
622 /// Copy a file atomically from source to destination. If the destination file already exists,
623 /// it must return Err(Error::FileAlreadyExists).
624 fn copy_atomic(&self, src: &Url, dest: &Url) -> DeltaResult<()>;
625
626 /// Write data to the specified path.
627 ///
628 /// If `overwrite` is false and the file already exists, this must return
629 /// `Err(Error::FileAlreadyExists)`.
630 fn put(&self, path: &Url, data: Bytes, overwrite: bool) -> DeltaResult<()>;
631
632 /// Perform a HEAD request for the given file at a Url, returning the file metadata.
633 ///
634 /// If the file does not exist, this must return an `Err` with [`Error::FileNotFound`].
635 fn head(&self, path: &Url) -> DeltaResult<FileMeta>;
636
637 /// Delete the file at the given path.
638 ///
639 /// This operation is idempotent: deleting a path that does not exist should return `Ok(())`.
640 /// For any other error, this must propagate the corresponding error.
641 fn delete(&self, path: &Url) -> DeltaResult<()>;
642}
643
644/// Provides JSON handling functionality to Delta Kernel.
645///
646/// Delta Kernel can use this handler to parse JSON strings into Row or read content from JSON
647/// files. Connectors can leverage this trait to provide their best implementation of the JSON
648/// parsing capability to Delta Kernel.
649pub trait JsonHandler: AsAny {
650 /// Parse the given json strings and return the fields requested by output schema as columns in
651 /// [`EngineData`]. json_strings MUST be a single column batch of engine data, and the
652 /// column type must be string
653 fn parse_json(
654 &self,
655 json_strings: Box<dyn EngineData>,
656 output_schema: SchemaRef,
657 ) -> DeltaResult<Box<dyn EngineData>>;
658
659 /// Read and parse the JSON format file at given locations and return the data as EngineData
660 /// with the columns requested by physical schema. Note: The [`FileDataReadResultIterator`]
661 /// must emit data from files in the order that `files` is given. For example if files ["a",
662 /// "b"] is provided, then the engine data iterator must first return all the engine data
663 /// from file "a", _then_ all the engine data from file "b". Moreover, for a given file, all
664 /// of its [`EngineData`] and constituent rows must be in order that they occur in the file.
665 /// Consider a file with rows (1, 2, 3). The following are legal iterator batches:
666 /// iter: [EngineData(1, 2), EngineData(3)]
667 /// iter: [EngineData(1), EngineData(2, 3)]
668 /// iter: [EngineData(1, 2, 3)]
669 /// The following are illegal batches:
670 /// iter: [EngineData(3), EngineData(1, 2)]
671 /// iter: [EngineData(1), EngineData(3, 2)]
672 /// iter: [EngineData(2, 1, 3)]
673 ///
674 /// Additionally, engines may not merge engine data across file boundaries.
675 ///
676 /// # Parameters
677 ///
678 /// - `files` - File metadata for files to be read.
679 /// - `physical_schema` - Select list of columns to read from the JSON file.
680 /// - `predicate` - Optional push-down predicate hint (engine is free to ignore it).
681 fn read_json_files(
682 &self,
683 files: &[FileMeta],
684 physical_schema: SchemaRef,
685 predicate: Option<PredicateRef>,
686 ) -> DeltaResult<FileDataReadResultIterator>;
687
688 /// Atomically (!) write a single JSON file. Each selected row of the input data must be
689 /// written as a new JSON object appended to the file; rows not selected by a batch's
690 /// selection vector (see [`FilteredEngineData`]) must not be written.
691 /// [`FilteredEngineData::apply_selection_vector`] produces the selected-rows view for
692 /// implementations that do not filter during serialization. This write must:
693 /// (1) serialize the selected rows to newline-delimited json (each row is a json object
694 /// literal)
695 /// (2) write the data to storage atomically (i.e. if the file already exists, fail unless the
696 /// overwrite flag is set)
697 ///
698 /// For example, the JSON data should be written as { "column1": "val1", "column2": "val2", .. }
699 /// with each row on a new line.
700 ///
701 /// NOTE: Null columns should not be written to the JSON file. For example, if a row has columns
702 /// ["a", "b"] and the value of "b" is null, the JSON object should be written as
703 /// { "a": "..." }. Note that including nulls is technically valid JSON, but would bloat the
704 /// log, therefore we recommend omitting them.
705 ///
706 /// # Parameters
707 ///
708 /// - `path` - URL specifying the location to write the JSON file
709 /// - `data` - Iterator of [`FilteredEngineData`] to write to the JSON file
710 /// - `overwrite` - If true, overwrite the file if it exists. If false, the call must fail if
711 /// the file exists.
712 fn write_json_file(
713 &self,
714 path: &Url,
715 data: DeltaResultIterator<'_, FilteredEngineData>,
716 overwrite: bool,
717 ) -> DeltaResult<()>;
718}
719
720/// Reserved field IDs for metadata columns in Delta tables.
721///
722/// These field IDs are reserved and should not be used for regular table columns.
723/// They are used to provide file-level metadata as virtual columns during reads.
724pub mod reserved_field_ids {
725 /// Reserved field ID for the file name metadata column (`_file`).
726 /// This column provides the name of the Parquet file that contains each row.
727 pub const FILE_NAME: i64 = 2147483646;
728}
729
730/// Metadata from a Parquet file footer.
731///
732/// This struct contains metadata extracted from a Parquet file's footer, including the schema.
733/// It is designed to be extensible for future additions such as row group statistics.
734#[derive(Debug, Clone)]
735pub struct ParquetFooter {
736 /// The schema of the Parquet file, converted to Delta Kernel's schema format.
737 pub schema: SchemaRef,
738}
739
740/// Provides Parquet file related functionalities to Delta Kernel.
741///
742/// Connectors can leverage this trait to provide their own custom
743/// implementation of Parquet data file functionalities to Delta Kernel.
744pub trait ParquetHandler: AsAny {
745 /// Read and parse the Parquet file at given locations and return the data as EngineData with
746 /// the columns requested by physical schema. The ParquetHandler _must_ return exactly the
747 /// columns specified in `physical_schema`, and they _must_ be in schema order.
748 ///
749 /// # Resolving Parquet schema to the physical schema
750 ///
751 /// When reading the Parquet file, the columns are resolved from the Parquet schema to the
752 /// kernel's `physical_schema`. To do so, the parquet reader must match each Parquet column
753 /// to a [`StructField`] in the `physical_schema`. All columns in the returned `EngineData`
754 /// must be in the same order as specified in `physical_schema`.
755 ///
756 /// Parquet columns are matched to `physical_schema` [`StructField`]s using the following rules:
757 /// 1. **Field ID**: If a [`StructField`] in `physical_schema` contains a field ID (specified in
758 /// [`ColumnMetadataKey::ParquetFieldId`] metadata), use the ID to match the Parquet column's
759 /// field id
760 /// 2. **Field Name**: If no field ID is present in the `physical_schema`'s [`StructField`] or
761 /// no matching parquet field ID is found, fall back to matching by column name
762 ///
763 /// # Type coercion
764 ///
765 /// A matched Parquet column whose physical type differs from the `physical_schema`
766 /// [`StructField`] must be coerced to the requested type. In particular, timestamp columns MUST
767 /// be normalized to the protocol specified microsecond precision: a `TIMESTAMP(MILLIS)` (or
768 /// any other non-microsecond unit) column read into a `TIMESTAMP` / `TIMESTAMP_NTZ` field
769 /// must be rescaled to microseconds (a finer unit such as nanosecond is truncated). The
770 /// default engine does this via `arrow::compute::cast` while reordering columns to the
771 /// requested schema.
772 ///
773 /// # Metadata Columns
774 ///
775 /// The ParquetHandler must support virtual metadata columns that provide additional information
776 /// about each row. These columns are not stored in the Parquet file but are generated at read
777 /// time.
778 ///
779 /// ## Row Index Column
780 ///
781 /// When a column in `physical_schema` is marked as a row index metadata column (via
782 /// [`StructField::create_metadata_column`] with [`schema::MetadataColumnSpec::RowIndex`]), the
783 /// ParquetHandler must populate it with the 0-based row position within the Parquet file:
784 ///
785 /// - **Column name**: User-specified (commonly `"row_index"` or `"_metadata.row_index"`)
786 /// - **Type**: `LONG` (non-nullable)
787 /// - **Values**: Sequential integers starting at 0 for each file
788 /// - **Use case**: Track row positions for downstream processing, or internally used to compute
789 /// Row IDs
790 ///
791 /// Example: A file with 5 rows would have row_index values `[0, 1, 2, 3, 4]`.
792 ///
793 /// ## File Name Column (Reserved Field ID)
794 ///
795 /// When a column in `physical_schema` has the reserved field ID
796 /// [`reserved_field_ids::FILE_NAME`] (2147483646), the ParquetHandler must populate it
797 /// with the file path/name:
798 ///
799 /// - **Column name**: `"_file"`
800 /// - **Type**: `STRING` (non-nullable)
801 /// - **Field ID**: 2147483646 (reserved)
802 /// - **Values**: The file path/URL (e.g., `"s3://bucket/path/file.parquet"`)
803 /// - **Use case**: Track which file each row came from in multi-file reads
804 ///
805 /// Example: All rows from the same file would have the same `_file` value.
806 ///
807 /// ## Metadata Column Examples
808 ///
809 /// ```rust,ignore
810 /// # use buoyant_kernel as delta_kernel;
811 /// use delta_kernel::schema::{StructType, StructField, DataType, MetadataColumnSpec};
812 ///
813 /// // Example 1: Schema with row_index metadata column
814 /// let schema_with_row_index = StructType::try_new([
815 /// StructField::nullable("id", DataType::INTEGER),
816 /// StructField::create_metadata_column("row_index", MetadataColumnSpec::RowIndex),
817 /// StructField::nullable("value", DataType::STRING),
818 /// ])?;
819 ///
820 /// // Example 2: Schema with _file metadata column (using reserved field ID)
821 /// let schema_with_file_path = StructType::try_new([
822 /// StructField::nullable("id", DataType::INTEGER),
823 /// StructField::create_metadata_column("_file", MetadataColumnSpec::FilePath),
824 /// StructField::nullable("value", DataType::STRING),
825 /// ])?;
826 /// ```
827 ///
828 /// ---
829 ///
830 /// If no matching Parquet column is found, `NULL` values are returned
831 /// for nullable columns in `physical_schema`. For non-nullable columns, an error is returned.
832 ///
833 ///
834 /// ## Column Matching Examples
835 ///
836 /// Consider a `physical_schema` with the following fields:
837 /// - Column 0: `"i_logical"` (integer, non-null) with field ID 1 (via
838 /// [`ColumnMetadataKey::ParquetFieldId`])
839 /// - Column 1: `"s"` (string, nullable) with no field ID metadata
840 /// - Column 2: `"i2"` (integer, nullable) with no field ID metadata
841 ///
842 /// [`ColumnMetadataKey::ParquetFieldId`]: crate::schema::ColumnMetadataKey::ParquetFieldId
843 ///
844 /// And a Parquet file containing these columns:
845 /// - Column 0: `"i2"` (integer, nullable) with field ID 3
846 /// - Column 1: `"i"` (integer, non-null) with field ID 1
847 /// - No `"s"` column present
848 ///
849 /// The column matching would work as follows:
850 /// - `"i_logical"` matches `"i"` by field ID (both have ID 1)
851 /// - `"i2"` matches `"i2"` by column name (no field ID to match on)
852 /// - `"s"` has no matching Parquet column, so NULL values are returned
853 ///
854 /// The returned data will contain exactly 3 columns in physical schema order:
855 /// `{i_logical: parquet[1], s: NULL.., i2: parquet[0]}`
856 ///
857 /// # Parameters
858 ///
859 /// - `files` - File metadata for files to be read.
860 /// - `physical_schema` - Select list and order of columns to read from the Parquet file.
861 /// - `predicate` - Optional push-down predicate hint (engine is free to ignore it).
862 ///
863 /// # Returns
864 /// A [`DeltaResult`] containing a [`FileDataReadResultIterator`].
865 /// Each element of the iterator is a [`DeltaResult`] of [`EngineData`]. The [`EngineData`]
866 /// has the contents of `files` and must match the provided `physical_schema`.
867 ///
868 /// Note: The [`FileDataReadResultIterator`] must emit data from files in the order that `files`
869 /// is given. For example if files ["a", "b"] is provided, then the engine data iterator must
870 /// first return all the engine data from file "a", _then_ all the engine data from file "b".
871 /// Moreover, for a given file, all of its [`EngineData`] and constituent rows must be in order
872 /// that they occur in the file. Consider a file with rows
873 /// (1, 2, 3). The following are legal iterator batches:
874 /// iter: [EngineData(1, 2), EngineData(3)]
875 /// iter: [EngineData(1), EngineData(2, 3)]
876 /// iter: [EngineData(1, 2, 3)]
877 /// The following are illegal batches:
878 /// iter: [EngineData(3), EngineData(1, 2)]
879 /// iter: [EngineData(1), EngineData(3, 2)]
880 /// iter: [EngineData(2, 1, 3)]
881 ///
882 /// Additionally, engines must not merge engine data across file boundaries.
883 ///
884 /// [`ColumnMetadataKey::ParquetFieldId`]: crate::schema::ColumnMetadataKey
885 fn read_parquet_files(
886 &self,
887 files: &[FileMeta],
888 physical_schema: SchemaRef,
889 predicate: Option<PredicateRef>,
890 ) -> DeltaResult<FileDataReadResultIterator>;
891
892 /// Write data to a Parquet file at the specified URL.
893 ///
894 /// This method writes the provided `data` to a Parquet file at the given `url`.
895 ///
896 /// This will overwrite the file if it already exists. For filesystem-backed
897 /// implementations, the parent directories must be created if they do not exist.
898 ///
899 /// # Parquet field IDs
900 ///
901 /// The engine must write a Parquet `field_id` correctly when the kernel
902 /// [`StructField`] carries a field-id related annotation, including:
903 /// - [`ColumnMetadataKey::ColumnMappingId`] / [`ColumnMetadataKey::ParquetFieldId`]
904 /// - [`ColumnMetadataKey::ColumnMappingNestedIds`]
905 ///
906 /// For how to use these keys, refer to the Delta protocol's [Column Mapping] and
907 /// [IcebergCompatV2] sections.
908 ///
909 /// **Non-compliance produces files with incorrect `field_id`s**, which may lead to
910 /// read failures when column mapping mode is `id` and to failures when converting
911 /// the table to Iceberg.
912 ///
913 /// # Parameters
914 ///
915 /// - `url` - The full URL path where the Parquet file should be written (e.g.,
916 /// `s3://bucket/path/file.parquet`).
917 /// - `data` - An iterator of engine data to be written to the Parquet file.
918 ///
919 /// # Returns
920 ///
921 /// A [`DeltaResult`] indicating success or failure.
922 ///
923 /// [`StructField`]: crate::schema::StructField
924 /// [`ColumnMetadataKey::ColumnMappingId`]: crate::schema::ColumnMetadataKey::ColumnMappingId
925 /// [`ColumnMetadataKey::ParquetFieldId`]: crate::schema::ColumnMetadataKey::ParquetFieldId
926 /// [`ColumnMetadataKey::ColumnMappingNestedIds`]: crate::schema::ColumnMetadataKey::ColumnMappingNestedIds
927 /// [Column Mapping]: https://github.com/delta-io/delta/blob/master/PROTOCOL.md#column-mapping
928 /// [IcebergCompatV2]: https://github.com/delta-io/delta/blob/master/PROTOCOL.md#iceberg-compatibility-v2
929 fn write_parquet_file(
930 &self,
931 location: url::Url,
932 data: DeltaResultIteratorStatic<Box<dyn EngineData>>,
933 ) -> DeltaResult<()>;
934
935 /// Read the footer metadata from a Parquet file without reading the data.
936 ///
937 /// This method reads only the Parquet file footer (metadata section), which is useful for
938 /// schema inspection, compatibility checking, and determining whether parsed statistics
939 /// columns are present and compatible with the current table schema.
940 ///
941 /// # Parameters
942 ///
943 /// - `file` - File metadata for the Parquet file whose footer should be read. The `size` field
944 /// should contain the actual file size to enable efficient footer reads without additional
945 /// I/O operations.
946 ///
947 /// # Returns
948 ///
949 /// A [`DeltaResult`] containing a [`ParquetFooter`] with the Parquet file's metadata, including
950 /// the schema converted to Delta Kernel's format.
951 ///
952 /// # Field IDs
953 ///
954 /// If the Parquet file contains field IDs (written when column mapping is enabled), they are
955 /// preserved in each [`StructField`]'s metadata. Callers can access field IDs via
956 /// [`StructField::get_config_value`] with [`ColumnMetadataKey::ParquetFieldId`].
957 ///
958 /// # Errors
959 ///
960 /// Returns an error if:
961 /// - The file cannot be accessed or does not exist
962 /// - The file is not a valid Parquet file
963 /// - The footer cannot be read or parsed
964 /// - The schema cannot be converted to Delta Kernel's format
965 ///
966 /// [`StructField`]: crate::schema::StructField
967 /// [`StructField::get_config_value`]: crate::schema::StructField::get_config_value
968 /// [`ColumnMetadataKey::ParquetFieldId`]: crate::schema::ColumnMetadataKey::ParquetFieldId
969 fn read_parquet_footer(&self, file: &FileMeta) -> DeltaResult<ParquetFooter>;
970}
971
972/// The `Engine` trait encapsulates all the functionality an engine or connector needs to provide
973/// to the Delta Kernel in order to read the Delta table.
974///
975/// Engines/Connectors are expected to pass an implementation of this trait when reading a Delta
976/// table.
977pub trait Engine: AsAny {
978 /// Get the connector provided [`EvaluationHandler`].
979 fn evaluation_handler(&self) -> Arc<dyn EvaluationHandler>;
980
981 /// Get the connector provided [`StorageHandler`]
982 fn storage_handler(&self) -> Arc<dyn StorageHandler>;
983
984 /// Get the connector provided [`JsonHandler`].
985 fn json_handler(&self) -> Arc<dyn JsonHandler>;
986
987 /// Get the connector provided [`ParquetHandler`].
988 fn parquet_handler(&self) -> Arc<dyn ParquetHandler>;
989
990 /// Get the connector provided [`PlanExecutor`].
991 ///
992 /// The default implementation returns a trivial executor that errors on every operation.
993 #[cfg(feature = "declarative-plans")]
994 fn plan_executor(&self) -> Arc<dyn PlanExecutor> {
995 Arc::new(())
996 }
997}
998
999// Rustdoc's documentation tests can do some things that regular unit tests can't. Here we are
1000// using doctests to test macros. Specifically, we are testing for failed macro invocations due
1001// to invalid input, not the macro output when the macro invocation is successful (which can/should
1002// be done in unit tests). This module is not exclusively for macro tests only so other doctests can
1003// also be added. https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#include-items-only-when-collecting-doctests
1004#[cfg(doctest)]
1005mod doctests;
1006
1007#[cfg(test)]
1008mod tests {
1009 use rstest::rstest;
1010
1011 use super::*;
1012
1013 #[rstest]
1014 #[case::zero(0, Some(0))]
1015 #[case::one(1, Some(1))]
1016 #[case::i64_max(i64::MAX as u64, Some(i64::MAX))]
1017 #[case::just_over_i64_max(i64::MAX as u64 + 1, None)]
1018 #[case::u64_max(u64::MAX, None)]
1019 /// Tests `FileMeta::size_as_i64` for both success (size fits in i64) and error (size exceeds
1020 /// i64::MAX) paths.
1021 fn test_file_meta_size_as_i64(#[case] size: u64, #[case] expected: Option<i64>) {
1022 let meta = FileMeta::new(Url::parse("file:///x").unwrap(), 0, size);
1023 match expected {
1024 Some(v) => assert_eq!(meta.size_as_i64().unwrap(), v),
1025 None => assert!(meta
1026 .size_as_i64()
1027 .unwrap_err()
1028 .to_string()
1029 .contains("exceeds i64::MAX")),
1030 }
1031 }
1032}