Skip to content

Commit c0a78c3

Browse files
authored
Merge branch 'main' into fd-remove
2 parents c6e8836 + 57ad014 commit c0a78c3

37 files changed

Lines changed: 1457 additions & 327 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ Keep this list updated when new protocol features are added to kernel.
315315
block closes, and emit via `warn!()` only then. (`on_event`'s visitor does no
316316
warning-eligible work, so it may run under the lock directly.) See
317317
`kernel/src/metrics/reporter.rs` for the canonical pattern.
318+
- **Keep tests with process-global state safe under concurrency:** `cargo test` runs tests as
319+
parallel threads in one test binary, while nextest normally runs each test in a separate
320+
process. Tests for global tracing subscribers and callbacks must not share capture buffers with
321+
thread-local dispatch tests or assume no other thread can emit an event.
318322

319323
## Code Style
320324

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datafusion-executor/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datafusion-executor/src/expression.rs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -390,13 +390,11 @@ fn struct_columns_from_patch(
390390
/// types come from `output_type`, which must be a struct holding only primitive fields (matching
391391
/// the kernel evaluator, which supports only primitive targets).
392392
///
393-
/// Each field extracts its value with `cast(get_field(map, name), T)`. For a numeric or temporal
394-
/// type the raw value is first wrapped in `nullif(.., '')`, mapping an empty string to null before
395-
/// the cast, so an empty string becomes null (kernel's `empty_string_partition_cast`) while an
396-
/// unparseable value fails the cast (kernel's hard parse error). String and Binary keep the raw
397-
/// value (empty is a valid empty string / empty bytes). A missing key or null value is already null
398-
/// via [`get_field`]. The whole struct is nulled where the input map row is null, via `<map> IS NOT
399-
/// NULL`.
393+
/// Each field extracts its value with `cast(get_field(map, name), T)`. For every type except String
394+
/// and Binary, the raw value is first wrapped in `nullif(.., '')`, mapping an empty string to null
395+
/// before the cast. String and Binary keep the raw value because empty strings and bytes are valid.
396+
/// A missing key or null value is already null via [`get_field`]. The whole struct is nulled where
397+
/// the input map row is null, via `<map> IS NOT NULL`.
400398
///
401399
/// KNOWN DIVERGENCES from the kernel parser, confined to malformed or non-spec-compliant values
402400
/// (spec-compliant writers never emit them):
@@ -408,14 +406,20 @@ fn struct_columns_from_patch(
408406
/// value's scale to match the target's exactly (and hard-errors otherwise).
409407
///
410408
/// # Errors
411-
/// Returns an error when `output_type` is absent, not a struct, or has a non-primitive field, or
412-
/// from lowering the map expression.
409+
///
410+
/// Returns an error when options are configured, `output_type` is absent, not a struct, or has a
411+
/// non-primitive field, or from lowering the map expression.
413412
fn map_to_struct_to_df_expr(
414413
map_to_struct: &MapToStructExpression,
415414
input_schema: &StructType,
416415
output_type: Option<&KernelDataType>,
417416
) -> DeltaResult<DFExpr> {
418417
let target = require_struct_output(output_type, "MapToStruct")?;
418+
if !map_to_struct.options.is_default() {
419+
return Err(Error::unsupported(
420+
"DataFusion execution of MapToStruct with configured options",
421+
));
422+
}
419423
let map = to_df_expr(&map_to_struct.map_expr, input_schema, None)?;
420424

421425
let mut args = Vec::with_capacity(target.num_fields() * 2);
@@ -547,7 +551,7 @@ mod tests {
547551
use datafusion::physical_expr::execution_props::ExecutionProps;
548552
use delta_kernel::expressions::{
549553
col, lit, null_lit, ColumnName as KernelColumnName, Expression as KernelExpr,
550-
ExpressionStructPatch, ExpressionStructPatchBuilder,
554+
ExpressionStructPatch, ExpressionStructPatchBuilder, MapToStructOptions,
551555
};
552556
use delta_kernel::schema::{schema, schema_ref, ArrayType, DataType, MapType, StructType};
553557
use rstest::rstest;
@@ -1001,7 +1005,7 @@ mod tests {
10011005
/// Lowers a `MapToStruct` over `pv` targeting `output_schema` and renders it as a `Display`
10021006
/// string.
10031007
fn lower_map_to_struct(output_schema: StructType) -> String {
1004-
let kernel = KernelExpr::map_to_struct(col!("pv"));
1008+
let kernel = KernelExpr::map_to_struct(col!("pv"), MapToStructOptions::default());
10051009
let target: DataType = output_schema.into();
10061010
to_df_expr(&kernel, &pv_map_schema(), Some(&target))
10071011
.unwrap()
@@ -1063,13 +1067,28 @@ mod tests {
10631067
#[case] output_type: Option<DataType>,
10641068
#[case] expected_message: &str,
10651069
) {
1066-
let kernel = KernelExpr::map_to_struct(col!("pv"));
1070+
let kernel = KernelExpr::map_to_struct(col!("pv"), MapToStructOptions::default());
10671071
let err = to_df_expr(&kernel, &pv_map_schema(), output_type.as_ref())
10681072
.unwrap_err()
10691073
.to_string();
10701074
assert!(err.contains(expected_message), "{err}");
10711075
}
10721076

1077+
#[test]
1078+
fn configured_map_to_struct_is_unsupported() {
1079+
let target = DataType::from(schema! { nullable "ts": TIMESTAMP });
1080+
let kernel = KernelExpr::map_to_struct(
1081+
col!("pv"),
1082+
MapToStructOptions::default().with_timestamp_timezone("America/Los_Angeles"),
1083+
);
1084+
1085+
let error = to_df_expr(&kernel, &pv_map_schema(), Some(&target))
1086+
.unwrap_err()
1087+
.to_string();
1088+
1089+
assert!(error.contains("MapToStruct with configured options"));
1090+
}
1091+
10731092
// === ParseJson Shared Helpers ===
10741093

10751094
/// Input schema for JSON tests: `{ j: string }`.

derive-macros/src/lib.rs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use proc_macro2::{Ident, Span, TokenStream};
2-
use quote::{quote, quote_spanned, ToTokens};
2+
use quote::{quote, quote_spanned};
33
use syn::parse::Parser;
44
use syn::punctuated::Punctuated;
55
use syn::spanned::Spanned;
@@ -551,23 +551,30 @@ pub fn internal_api(
551551
item: proc_macro::TokenStream,
552552
) -> proc_macro::TokenStream {
553553
let input = parse_macro_input!(item as Item);
554+
internal_api_impl(input).into()
555+
}
554556

557+
fn internal_api_impl(input: Item) -> TokenStream {
555558
// Create a version with public visibility for the unstable feature
556-
let public_version = make_public(input.clone());
559+
let public_version = match make_public(input.clone()) {
560+
Ok(public_version) => public_version,
561+
Err(err) => {
562+
let error = err.to_compile_error();
563+
return quote! { #input #error };
564+
}
565+
};
557566

558567
// The original item stays as-is for the non-unstable case
559-
let output = quote! {
568+
quote! {
560569
#[cfg(feature = "internal-api")]
561570
#public_version
562571

563572
#[cfg(not(feature = "internal-api"))]
564573
#input
565-
};
566-
567-
output.into()
574+
}
568575
}
569576

570-
fn make_public(mut item: Item) -> Item {
577+
fn make_public(mut item: Item) -> Result<Item, Error> {
571578
/// Transforms the passed visibility to be `pub`. We pass the original span that the visibility
572579
/// came from, and attach it to the newly created pub token. This means that the compiler treats
573580
/// it as user-written code and normal lints apply. We want this because it allows us to catch
@@ -591,7 +598,7 @@ fn make_public(mut item: Item) -> Item {
591598
}};
592599
}
593600

594-
let result = match &mut item {
601+
match &mut item {
595602
Item::Fn(f) => set_vis!(f),
596603
Item::Struct(s) => set_vis!(s),
597604
Item::Enum(e) => set_vis!(e),
@@ -606,24 +613,30 @@ fn make_public(mut item: Item) -> Item {
606613
item.span(),
607614
format!("unsupported item type for #[internal_api]: {item:?}"),
608615
)),
609-
};
610-
611-
if let Err(err) = result {
612-
let error = err.to_compile_error();
613-
let mut tokens = item.to_token_stream();
614-
tokens.extend(error);
615-
return syn::parse_quote!(#tokens);
616-
}
617-
618-
item
616+
}?;
617+
Ok(item)
619618
}
620619

621620
#[cfg(test)]
622621
mod tests {
623622
use rstest::rstest;
623+
use syn::parse_quote;
624624

625625
use super::*;
626626

627+
#[test]
628+
fn internal_api_rejects_public_items_without_panicking() {
629+
let input = parse_quote!(
630+
pub fn already_public() {}
631+
);
632+
633+
let output = internal_api_impl(input).to_string();
634+
635+
assert!(output.contains("pub fn already_public"));
636+
assert!(output.contains("compile_error"));
637+
assert!(output.contains("item is already public"));
638+
}
639+
627640
/// Expand `gen_schema_fields` for `input` and return the generated tokens as a string. Macro
628641
/// errors are embedded as `compile_error!` tokens in that string; `Err` only signals that the
629642
/// input itself failed to parse as a `DeriveInput`.

docs/user-guide/src/ffi/overview.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ engine-owned memory.
161161
| `visit_schema` | Walk a `SharedSchema` by invoking per-field callbacks on an `EngineSchemaVisitor` |
162162
| `visit_protocol` | Invoke a `visit_versions` callback, then a `visit_feature` callback per reader/writer feature |
163163
| `visit_metadata` | Invoke a single callback with `(id, name, description, format_provider, has_created_time, created_time_ms)` |
164+
| `visit_metadata_format_options` | Iterate arbitrary format option key/value pairs from a `SharedMetadata` handle |
164165
| `visit_metadata_configuration` | Iterate the `configuration` key/value map (takes a snapshot handle, not a metadata handle) |
165166
| `visit_string_map` / `get_from_string_map` | Iterate or look up entries in an opaque `CStringMap` (used by both metadata and scan-metadata surfaces) |
166167

@@ -383,7 +384,7 @@ pattern is the same in every case:
383384

384385
Callbacks run synchronously on the same thread that called `visit_*`. Strings
385386
passed to callbacks (`KernelStringSlice`) are borrowed for the duration of the
386-
call; copy them if you need to retain them beyond the callback.
387+
call. Copy them if you need to retain them beyond the callback.
387388

388389
## Error handling
389390

ffi/src/domain_metadata.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ use crate::{
99
OptionalValue, SharedExternEngine, SharedSnapshot, TryFromStringSlice,
1010
};
1111

12+
/// The row-tracking high-water mark before any fresh row IDs have been assigned.
13+
pub const ROW_TRACKING_INITIAL_HIGH_WATER_MARK: i64 = -1;
14+
15+
const _: [(); 1] = [(); (ROW_TRACKING_INITIAL_HIGH_WATER_MARK
16+
== delta_kernel::ROW_TRACKING_INITIAL_HIGH_WATER_MARK) as usize];
17+
1218
/// Get the domain metadata as an optional string allocated by `AllocatedStringFn` for a specific
1319
/// domain in this snapshot
1420
///
@@ -40,6 +46,29 @@ fn get_domain_metadata_impl(
4046
.and_then(|config| allocate_fn(kernel_string_slice!(config))))
4147
}
4248

49+
/// Get the row-tracking high-water mark for this snapshot.
50+
///
51+
/// Returns [`OptionalValue::None`] when the snapshot has no active `delta.rowTracking` domain
52+
/// metadata. [`OptionalValue::Some`] with a value of `ROW_TRACKING_INITIAL_HIGH_WATER_MARK` means
53+
/// row tracking is active but no row IDs have been assigned yet. Returns an error if the domain
54+
/// metadata cannot be read or its JSON configuration is malformed.
55+
///
56+
/// # Safety
57+
///
58+
/// Caller is responsible for passing valid snapshot and engine handles.
59+
#[no_mangle]
60+
pub unsafe extern "C" fn snapshot_row_tracking_high_water_mark(
61+
snapshot: Handle<SharedSnapshot>,
62+
engine: Handle<SharedExternEngine>,
63+
) -> ExternResult<OptionalValue<i64>> {
64+
let engine_ref = unsafe { engine.as_ref() };
65+
let snapshot = unsafe { snapshot.as_ref() };
66+
snapshot
67+
.get_row_tracking_high_water_mark(engine_ref.engine().as_ref())
68+
.map(OptionalValue::from)
69+
.into_extern_result(&engine_ref)
70+
}
71+
4372
/// Signature of the callback invoked once per clustering column by
4473
/// [`visit_clustering_columns`], in the order the columns appear in the `delta.clustering`
4574
/// domain. Each invocation describes one column:

0 commit comments

Comments
 (0)