Skip to content

duckdb: support struct extract column indices #10297

Description

@myrrc

Duckdb has nested struct accessors like x.y.z. This generally produces a get_item expression which we handle in filters, so WHERE x.y.z = 1 will not read x into memory, but will read only x.y.z. However, projection pushdown doesn't support this, so SELECT x.y.z reads x.

Duckdb has another mechanism to optimize this which is a TableFunction callback supports_pushdown_extract. If you define it, you get special ColumnIndex with type STRUCT_EXTRACT.

In order to support this, we need to:

  1. Teach Projection::new and Filter::new how to handle extract column indices. We need to convert these to a chain of get_item expressions. We also need to generate unique names when packing the output struct.
  2. Handle casts inside struct extract columns if extracted column type differs from a LogicalType for this column.
  3. Return no stats for extracted columns since Vortex doesn't support nested stats (yet).
  4. Forbid struct extract columns in aggregation (preferred) or change aggregation optimizer pass to support them.

Activity

  1. paoValle commented on Oct 9, 2026

    @paoValle

    I started on this and got steps 1–3 working on the scan side (paths across the FFI, get_item chain with the cast, no stats for extracted columns, plus the aggregation guard via duckdb_reader_is_aggregate) — draft PR #10405.

    It does not land yet: with the callback enabled, SELECT s.y, s.x.b, s.x.a dies inside DuckDB with

    INTERNAL Error: ExpressionExecutor::Execute called with a result vector of type INTEGER
    that does not match expression type STRUCT(x STRUCT(a INTEGER, b INTEGER), y INTEGER)
      at MultiFileReader::FinalizeChunk
    

    MultiFileColumnMapper::MapColumnStruct rebuilds the struct from the selected children and never checks IsPushdownExtract(), so FinalizeChunk applies a struct expression to a vector typed as the extracted field. ColumnIndexType::PUSHDOWN_EXTRACT looks like storage-table_scan-only in 1.5.5, and no MFR-based reader sets the callback.

    So the last piece is a DuckDB-side change in multi_file_column_mapper.cpp. Is a patch under vortex-duckdb/patches/ the route you want for that, or is there something an MFR can set to tell it the scan already returned the leaf? The draft has the scan side and a #[ignore]d e2e test that reproduces the failure, if it helps to judge.

  2. myrrc commented on Oct 9, 2026

    @myrrc
    ContributorAuthor
  3. paoValle commented on Oct 9, 2026

    @paoValle

    You are right, and I had the diagnosis before the conclusion the wrong way round. I went and looked at the pinned DuckDB, because that also answers where the retry has to happen.

    On develop's pin, DuckDB 1.5.5 (and the same holds for the newest release, v1.5.6):

    • src/common/multi_file/multi_file_column_mapper.cpp has no IsPushdownExtract() at all, so MapColumnStruct rebuilds the struct from the selected children and FinalizeChunk then applies a struct expression to a vector sized for the leaf. That is exactly the INTERNAL Error I hit.
    • extension/parquet/parquet_multi_file_info.cpp never sets supports_pushdown_extract either, so on that pin there is no MFR-based reader to copy from.

    At myrrc/duckdb-2.0's pin, DuckDB 561522aea, both are present: IsPushdownExtract() in the mapper, and supports_pushdown_extract = ParquetScanSupportPushdownExtract in Parquet. So the temporary patch is what made this work on 1.5.5, and removing it in 2.0 is right because it is upstreamed. "The last piece is a patch under patches/" was the 1.5.5 workaround, not the solution.

    Next on my side: rebase feat/duckdb-struct-extract-pushdown onto myrrc/duckdb-2.0, re-enable the #[ignore]d nested test and see whether the scan side holds unchanged.

    Two questions, and I will follow whichever you prefer:

    1. Is myrrc/duckdb-2.0 (Duckdb 2.0 #9141) the base you meant, or should I wait for it to land on develop first?
    2. If waiting, would you rather I keep [WIP] feat(duckdb): read pushed-down struct extracts #10405 as a draft on develop with the callback off until the pin moves, or close it and open it fresh on the new base?

    One thing that may be useful either way: remove_unused_columns.cpp disables pushdown extract when get.function.statistics is set (statistics_extended must be used, or statistics left NULL). We already use statistics_extended, but it is a silent failure mode for any reader that sets both.

  4. paoValle commented on Oct 9, 2026

    @paoValle

    Rebased onto myrrc/duckdb-2.0 and you were right. The nested test is un-ignored and green:

    cargo test -p vortex-duckdb --lib
    test result: ok. 216 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out
    

    cargo fmt --check is clean too.

    One part of the diff changed, and it got smaller rather than bigger: on 1.5.5 I had to override fn.statistics_extended to report no statistics for an extracted path. On 2.0 that override is redundant, because DuckDB's own MultiFileScanStatsExtended already returns nothing for an IsPushdownExtract() column and for a column read as a type other than the stored one. So that piece is gone rather than reworked.

    One note for #9141: it is 92 commits behind develop, so I took only my own commit with git rebase --onto myrrc/duckdb-2.0 develop HEAD. A plain git rebase myrrc/duckdb-2.0 tries to replay all of develop's commits onto the branch, which is the other direction.

    I have not pushed the rebased branch. #10405 can be updated as soon as #9141 lands on develop, which is what I would default to. If you would rather review the diff against the 2.0 pin right now, I can retarget the PR at myrrc/duckdb-2.0 and force-push the rebased history instead. Which do you prefer?

  5. myrrc commented on Oct 9, 2026

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions