Skip to content

Expr::Not and PhysicalExpr::NotExpr have different metadata propagation rules #27155

Open

Description

@paleolimbot

Describe the bug

The logical an physical representations of NOT propagate metadata differently.

I'm actually not sure which one is more correct...some metadata uses like "sensor" or "where this value came from" probably are not lying after a NOT operation; however, it's also a little like a function call which would discard its output metadata.

To Reproduce

#[test]
fn not_metadata_differs_between_logical_and_physical() -> Result<()> {
    let schema = Schema::new(vec![Field::new("flag", DataType::Boolean, false)]);
    let df_schema = DFSchema::try_from(schema.clone())?;

    let metadata = FieldMetadata::from(HashMap::from([(
        "source".to_string(),
        "input".to_string(),
    )]));

    let expr = Expr::Not(Box::new(
        col("flag").alias_with_metadata("flag", Some(metadata)),
    ));

    let logical_field = expr.to_field(&df_schema)?.1;
    let physical_expr = create_physical_expr(
        &expr,
        &df_schema,
        &ExecutionProps::new(),
        &PhysicalPlanningContext::default(),
    )?;
    let physical_field = physical_expr.return_field(&schema)?;

    // Logical NOT creates a metadata-free Boolean field.
    assert_eq!(logical_field.metadata().get("source"), None);

    // Physical NotExpr forwards its child's field metadata.
    assert_eq!(
        physical_field.metadata().get("source").map(String::as_str),
        Some("input")
    );

    Ok(())
}

Expected behavior

I'd expect the result to match

Additional context

No response

Willingness to contribute

I would like to contribute a fix for this bug, but request guidance from the DataFusion community

Activity

  1. added theissue type on Nov 6, 2026
  2. james-willis commented on Nov 10, 2026

    @james-willis
    Contributor

    copying a comment I left on #27143:

    I wish this just wouldn't plan but I didn't make the change in this or my prior PR (#24865) because of some unpleasant implications.

    In my prior planning PR we made mismatched "ARROW:extension:name" an error but the pair of a present and a null one goes to the present value.

    IMO this should be a planning error because ARROW:extension:name really defines the type and types should match between branches but it leads to some unpleasant situations especially around literals, for example COALESCE(json_col, '{}') would fail.

    This pain is just kind of a side effect of ARROW:extension:name being an insufficient type system. Here its limitation is that it cant describe what kinds of up/down casts are allowed.

  3. paleolimbot commented on Nov 10, 2026

    @paleolimbot
    MemberAuthor

    Yes, to do this properly we need a pluggable "common type" calculation and definition of a cast between two arbitrary types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions