Skip to content

All struct-aware optimizations are hardcoded to GetFieldFunc #21306

Description

@friendlymatthew

Is your feature request related to a problem or challenge?

Every optimization for struct field querying (row level pushdown, leaf-level projection pruning, PushdownChecker, resolve_struct_field_leaves) is gated on the GetFieldFunc udf.

A custom UDF like variant_get gets none of these optimizations. There's no trait or capability API for a UDF to declare "I'm a struct field access."

This blocks Variant support entirely unless variant_get decomposes to get_field chains or the Parquet source learns a general mechanism for struct-access UDFs

Activity

  1. adriangb commented on Apr 1, 2026

    @adriangb
    Contributor

    There's no trait or capability API for a UDF to declare "I'm a struct field access."

    There is:

    /// Returns placement information for this function.
    ///
    /// This is used by optimizers to make decisions about expression placement,
    /// such as whether to push expressions down through projections.
    ///
    /// The default implementation returns [`ExpressionPlacement::KeepInPlace`],
    /// meaning the expression should be kept where it is in the plan.
    ///
    /// Override this method to indicate that the function can be pushed down
    /// closer to the data source.
    fn placement(&self, _args: &[ExpressionPlacement]) -> ExpressionPlacement {
    ExpressionPlacement::KeepInPlace
    }

  2. liningpan commented on Jul 16, 2026

    @liningpan
    Contributor

    I have a query calling a UDF that returns a struct (e.g., fields a and b in the struct). If I manually unnest the struct (e.g., func(x)['a'] and func(x)['b']), datafusion will call my UDF twice and can not optimize it into one call (if I'm reading the execution plan and metrics correctly). It will do the same even if I take the UDF call to a CTE or separate view, so it seems more like an issue with optimization.

    I'm not sure if this is the root cause of this behavior.

  3. adriangb commented on Jul 16, 2026

    @adriangb
    Contributor

    @liningpan I don't think that's related to this issue, but it is a very valid point. Could you maybe open an issue to track that? Off the top of my head it feels like something the CSE (Common Sub-expression Elimination) optimizer might be able to handle. A good first step would be an MRE / benchmark showing the perf issue (you could write a UDF that has a sleep in it or something).

  4. raghuvanshraj commented on Nov 4, 2026

    @raghuvanshraj

    A measured data point for the Variant case, from #27007 (closing that as a duplicate of this).

    One 100k-row Parquet file, two columns with the same 20 fields: s as a plain struct, v as a Variant shredded on all 20. Reading one small field:

    query scan projection bytes_scanned
    SELECT s['f0'] FROM t get_field(s, f0) 11.2 KB
    SELECT variant_get(v, 'f0', 'BIGINT') FROM t variant_get(v, f0) 126.4 MB

    The f0 leaf exists on the Variant side at the same size (1,147 bytes per row group) and is never read. With a WHERE on the field, the struct predicate reaches the scan; the Variant one runs in a FilterExec after decoding 126 MB. Standalone reproducer and raw EXPLAIN ANALYZE output in #27007.

    For variant_get the capability needs two things beyond get_field: one argument may require two leaves (the typed leaf and the value fallback), and pruning must keep any row group that holds a fallback row. Of the two open PRs, #26282 does both; #25013 does not cover pruning. Happy to test either against shredded and unshredded fixtures. This is the first workstream in #26517.

  5. adriangb commented on Nov 4, 2026

    @adriangb
    Contributor

    In my mind what needs to happen here is:

    1. Variant provides an impl of PhysicalExprAdapter to be hooked up to the parquet scan that reads from shredded columns when present in that particular file.
    2. variant_get et al need to be marked to be pushed down into scans.
  6. raghuvanshraj commented on Nov 4, 2026

    @raghuvanshraj

    In my mind what needs to happen here is:

    1. Variant provides an impl of `PhysicalExprAdapter` to be hooked up to the parquet scan that reads from shredded columns when present in that particular file.
    
    2. `variant_get` et al need to be marked to be pushed down into scans.
    

    @adriangb makes sense, thanks. I'll start with the adapter in datafusion-variant since the factory already gets the file schema, so no core change needed for that part. The one thing I want to get right is a plain SELECT v on an unshredded file: it should rebuild the row from value rather than null-fill typed_value.

    For the pushdown side, do you lean towards #25013 or #26282? I'd rather build variant_get against whichever you expect to land.

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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions