Skip to content

[Data] Support predicate pushdown at the Delta Lake level - #65142

Open
tobby168 wants to merge 6 commits into
ray-project:masterfrom
tobby168:data/delta-predicate-pushdown
Open

[Data] Support predicate pushdown at the Delta Lake level#65142
tobby168 wants to merge 6 commits into
ray-project:masterfrom
tobby168:data/delta-predicate-pushdown

Conversation

@tobby168

@tobby168 tobby168 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

read_delta resolves the entire Delta table on the driver before any predicate is known:

DeltaTable(path) -> dt.to_pyarrow_dataset() -> ParquetDatasource.from_pyarrow_dataset()

Every file is enumerated at call time. For tables that rely on Delta's Z-ordering and per-file statistics rather than Hive partitioning, a selective query still touches every file — at minimum for tail metadata.

This routes read_delta through the DataSourceV2 pipeline (already the default) with a Delta-native file indexer that prunes files from the transaction log — by partition values and by per-file min/max statistics — before any file is listed, sized, or scheduled.

On an unpartitioned table of 4 files holding disjoint val ranges (the shape the issue describes):

predicate rows (V1 / V2) files listed
val > 250 10 / 10 1 of 4
val < 5 5 / 5 1 of 4
val > 1000 0 / 0 0 of 4
val >= 0 40 / 40 4 of 4
val != 3 39 / 39 4 of 4

Closes #61547.

Design

Log-level pruning is an optimization, never a correctness requirement.

Predicates reach the scanner through the existing ReadFiles.apply_predicate path and are enforced there while reading — data predicates through the PyArrow scanner filter, partition predicates by parsing the file path. PushdownDeltaFilePruning runs after PredicatePushdown and additionally copies them onto the upstream ListFiles indexer, so files that cannot match are never listed. If that rule doesn't fire, the query reads more files but returns the same rows.

Making that true rather than merely intended took two changes, both of which earlier revisions of this PR got wrong:

  • Partition predicates are no longer enforced by path parsing. PathPartitionParser.evaluate_predicate_on_partition swallows any evaluation error and keeps the file — sound only while something else applies the predicate, but apply_predicate removes the Filter as soon as a scanner accepts one. A path is a lossy view of the log: a null partition is the literal __HIVE_DEFAULT_PARTITION__ directory, and pc.is_in over the null-typed row the parser builds raises. Scanners can now decline enforcement via enforces_partition_predicate; DeltaScanner does, so the predicate stays in a Filter that binds against the partition columns the reader has already materialized — exactly typed, with real nulls. The rule still forwards it to the log for skipping, so a partition-pruned read still lists 1 of 2 files.
  • The rule declares PredicatePushdown in dependencies(). Ruleset only honors declared edges; list position is a tiebreaker, so an ordering assumption left in a comment can be silently inverted by an unrelated rule.

Statistics-based skipping is conservative: a file is dropped only when [min, max] proves no row can match. !=, NOT, UDFs, missing stats, a null bound, an unknown column — every "don't know" keeps the file. Over-retention costs a read; dropping a matching file loses data silently. test_pruning_never_drops_a_matching_file asserts that property over generated inputs (seeded per parameter, so each case is reproducible).

DeltaScanner subclasses ParquetScanner and adds exactly one thing — declining partition-predicate enforcement. Filter pushdown, column pruning and limit pushdown are inherited unchanged, and the reader is ParquetFileReader as-is, because a Delta table's data files are ordinary Parquet laid out Hive-style. Schema and partition columns come from the log, so no Parquet footers are sampled for inference.

The snapshot version is resolved on the driver and pinned into the datasource. Listing is deferred to workers, so leaving it unresolved would let a concurrent commit be read against a schema inferred before it existed, and could make a retried task disagree with its first attempt.

When V2 is used

_delta_table_supports_datasource_v2 is the single gate, and it fails closed — anything V2 would answer differently rather than merely faster stays on V1, so enabling V2 never changes a result:

  • Column mapping. The Parquet columns carry generated names while the logical names live only in the log, so opening the files directly with the logical schema finds none of the requested columns. to_pyarrow_dataset currently raises for these, so behavior is unchanged — and if [data] Fix delta column mapping read #64929 lands they pick up its fix without this path needing to know about it.
  • A column named path. That is how the reader labels the synthesized file path, so a real column of that name would be shadowed rather than returned.
  • **arrow_parquet_args passed. V1 forwards these to iter_batches; V2 has nowhere to put them, so honoring them means staying on V1 rather than silently dropping them.

Deletion Vectors are rejected up front on both paths. Those data files still physically contain the deleted rows, and the V2 path opens them directly. The previous error text suggested upgrading to deltalake>=0.10.0, which is not the issue under 1.5.0.

Incidental correctness fixes

Cases where the V2 path would otherwise have been silently wrong:

  • Path encoding. Add actions record paths URL-encoded while the objects they name are not — a partition value of e=f lives in grp=e%3Df but is recorded as grp=e%253Df. Joining the raw value yields a path that doesn't exist. Decoded once before joining, which reproduces DeltaTable.file_uris() without a second listing.
  • Column order. The V2 reader appends path-derived partition columns after the file's own and only reorders to an explicit projection, so partitioned tables would come back with partition columns moved to the end — unlike every previous release. The scanner now pins a projection, with path added back when include_paths is set so a column projection can't drop it.
  • Null partition values. Delta writes these as __HIVE_DEFAULT_PARTITION__, which cannot be cast to the column's declared type. Adds opt-in Partitioning.null_fallback, and PathPartitionParser no longer coerces the resulting None. read_parquet behavior is unchanged.
  • URI-addressed tables. deltalake resolves its storage backend from the URI while PyArrow rejects a scheme-prefixed path, so the log is opened with the path as given and the reader gets filesystem-native paths. Without this every s3:// / az:// table — read_delta's own documented examples — failed at execution. This was also the remaining half of the catalog= breakage.
  • count(). PushdownCountFiles asserted its indexer was a NonSamplingFileIndexer; it only needs one that can be pinned to whole-file listing. It now checks for that and skips the optimization otherwise, rather than failing the query.
  • An empty Delta table is answered from the log — honoring columns and include_paths, so unioning it with a populated table doesn't fail on mismatched schemas — instead of tripping the V2 "no files found" guard, which exists to catch empty directories.

Not a duplicate

No open PR addresses #61547 (gh pr list --state open --search "61547 in:body" returns nothing). #62959 was an earlier attempt by the same author on a since-superseded architecture; it was closed by the stale bot without human review, and this is a fresh implementation against the current DataSourceV2 code. richardliaw invited work on the issue in this comment.

The nearest open PR in this area, #64929 (Delta column mapping), touches read_delta in the same function but solves a different problem; the column-mapping fallback above is written so the two compose rather than conflict.

Testing

Run per file, matching how Bazel runs these targets. 270 passed, 0 failed.

pytest -q python/ray/data/tests/unit/test_delta_file_pruning.py                      # 57 passed
pytest -q python/ray/data/_internal/datasource_v2/tests/test_delta_file_indexer.py   # 19 passed
pytest -q python/ray/data/tests/datasource/test_delta.py                             # 15 passed
pytest -q python/ray/data/tests/datasource/test_delta_pushdown.py                    # 64 passed
pytest -q python/ray/data/tests/datasource/test_write_delta.py                       # 58 passed
pytest -q python/ray/data/tests/datasource/test_read_parquet_v2.py                   # 13 passed
pytest -q python/ray/data/tests/test_execution_optimizer_basic.py                    # 18 passed
pytest -q python/ray/data/tests/test_execution_optimizer_limit_pushdown.py           # 26 passed
pytest -q python/ray/data/tests/test_execution_optimizer_advanced.py                 # 45 passed

pre-commit run --files $(git diff --name-only master...HEAD) — all hooks pass.

Tests assert on files skipped, not only on rows: reading every file also produces the right answer, so a pruning optimization that silently stopped pruning would otherwise pass every correctness test. Likewise test_count_matches_v1 counts on a fresh dataset, because reading first populates the cached row count and hides the plan shape count() actually takes.

test_parquet.py, test_partitioning.py and python/ray/data/tests/unit/ show pre-existing failures in the local environment used here (missing torch, S3/moto fixtures, and Ray workers unable to import test modules outside Bazel). Each was verified identical on the base commit — test_parquet.py 7 failed / 34 errors on both, test_partitioning.py 32 failed / 6 errors on both, tests/unit/ 9 failed on both.

AI assistance

This change was developed with AI assistance (Claude Code), including two adversarial review passes. The first found the count(), **arrow_parquet_args and catalog= breakage; the second found that partition predicates still had a single, fail-open enforcement point — returning extra rows for is_in over a nullable partition column in the default configuration — and that URI-addressed tables did not work at all. Both are fixed and covered by tests above.

tobby168 and others added 5 commits July 30, 2026 13:11
Groundwork for deferring Delta Lake file resolution to execution time
(ray-project#61547).

`prune_add_actions` answers, from a Delta snapshot's add actions alone,
which data files a query could need. Partition values are matched exactly
via Ray's own expression evaluator; data columns are bounded conservatively
against the log's min/max statistics, so a file is dropped only when its
interval proves no row can match. Every unprovable case keeps the file --
over-retention costs a read, but dropping a matching file loses data
silently.

`DeltaFileIndexer` lists a table from that log rather than walking the
filesystem, applying the pruning before files are sized or chunked. Paths
are reconstructed by decoding the log's URL-encoded relative path once
before joining; the raw value names a path that does not exist when a
partition value contains `=`, `/` or a space.

`build_manifests` is extracted from `NonSamplingFileIndexer` unchanged so
both indexers share manifest batching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
`read_delta` now builds a `DeltaDatasourceV2` when `use_datasource_v2` is
set (the default), so listing is deferred to execution and driven by the
Delta log rather than `to_pyarrow_dataset()`, which resolved every file on
the driver before any predicate was known (ray-project#61547).

`PushdownDeltaFilePruning` runs after `PredicatePushdown` and copies the
predicates it settled onto the scanner into the upstream `ListFiles`
indexer. Files that cannot match are then never listed, sized, or
scheduled. Pruning is strictly an optimization: the scanner still enforces
the predicates, so a plan the rule does not recognize reads more files but
returns the same rows.

Schema and partition columns come from the log, so no Parquet footers are
sampled. The scanner pins a projection to keep the Delta column order --
the reader appends path-derived partition columns, which would otherwise
move them to the end of every partitioned table.

Also handled:
- Deletion Vectors are rejected up front on both paths. Reading those files
  directly returns rows the table marks deleted; the previous error text
  suggested upgrading to deltalake>=0.10.0, which is not the issue under
  1.5.0.
- `Partitioning.null_fallback` parses a sentinel directory name back to
  `None`. Delta writes null partition values as
  `__HIVE_DEFAULT_PARTITION__`, which cannot be cast to the column's
  declared type. Opt-in, so `read_parquet` is unchanged.
- An empty Delta table is answered from the log instead of tripping the V2
  "no files found" guard, which exists to catch empty directories.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
Under `delta.columnMapping.mode`, a table's Parquet files carry generated
column names and the logical names live only in the transaction log. The
V2 path opens those files directly with the logical schema, so it would
ask for names the files don't have and hand back empty columns.

Route such tables to the V1 path instead of guarding against them. Today
`to_pyarrow_dataset` raises for them, so behavior is unchanged; if ray-project#64929
lands, they pick up its fix without this path needing to know about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
Review of ray-project#65142 found the V2 read path diverging from V1 in ways that
were silent, plus a documented invariant the code did not actually hold.

`count()` crashed: `PushdownCountFiles` asserted the indexer was a
`NonSamplingFileIndexer`. It only needs one that can be pinned to
whole-file listing, so it now checks for that and skips the optimization
otherwise instead of failing the query. The PR's own test hid this by
reading before counting, which populated the cached row count.

`**arrow_parquet_args` was accepted and dropped, changing which rows came
back. `catalog=` broke: its bucket-rooted filesystem was wrapped in a
table-rooted `SubTreeFileSystem` for `to_pyarrow_dataset`, but the V2 path
resolves absolute paths from the log. Both now decide V2 eligibility in
one place, `_delta_table_supports_datasource_v2`, which fails closed --
anything V2 would answer *differently* rather than merely faster stays on
V1.

The claim that log-level pruning is only an optimization did not hold for
typed partition columns: partition values parsed from a path are strings,
so `col("year") == lit(2024)` raised inside PyArrow and was conservatively
read as "keep the file", leaving pruning as the only thing enforcing the
predicate. The partitioning now carries `field_types` from the Delta
schema so the scanner can evaluate such predicates itself, and tables
whose partition types that cannot express (date, decimal) stay on V1.
`PathPartitionParser` also no longer coerces a `null_fallback`-derived
`None`, which the two features together would otherwise hit.

Also: the snapshot version is pinned on the driver, so deferred listing
can't pick up a concurrent commit or disagree across task retries; `path`
survives a column projection under `include_paths`; an empty table honors
`columns` and `include_paths`; the rule declares `PredicatePushdown` as a
dependency rather than relying on list position; the datasource caches its
snapshot instead of reopening the log five times per read; and listing
uses `ParquetFileChunker`, matching `read_parquet`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
A second review found the V2 Delta path returning extra rows in its
default configuration, and failing outright for URI-addressed tables.

`PathPartitionParser.evaluate_predicate_on_partition` swallows any
evaluation error and keeps the file. That is sound only while something
else applies the predicate -- but `ReadFiles.apply_predicate` removes the
`Filter` as soon as a scanner accepts one, leaving path parsing as the
sole enforcement point. A path is a lossy view of what the Delta log
records: a null partition is the literal `__HIVE_DEFAULT_PARTITION__`
directory, and `pc.is_in` over the null-typed row the parser builds
raises. `col("p").is_in(["a"]) | (col("p") == lit("b"))` over a nullable
partition column therefore returned the NULL partition's rows as well.

Scanners can now decline to enforce a partition predicate
(`enforces_partition_predicate`), in which case the optimizer keeps it in
a `Filter` above the read, where it binds against the partition columns
the reader has already materialized -- exactly typed, with real nulls.
`DeltaScanner` declines. `PushdownDeltaFilePruning` still forwards the
same predicate to the log for file skipping, so pruning is unchanged and
only the correctness burden moves; a partition-pruned read still lists 1
of 2 files.

This also removes the reason to keep exotic partition types off V2: they
now simply prune less rather than risk a wrong answer.

`DeltaDatasourceV2` also never resolved its paths, so manifests carried
scheme-prefixed URIs and PyArrow rejected them at read time -- breaking
every `s3://`/`az://` table, including `read_delta`'s own docstring
examples, with V2 on by default. The log is now opened with the URI as
given while the reader gets filesystem-native paths. This was also the
remaining half of the `catalog=` breakage.

Tables with a column named `path` stay on V1; that name is how the reader
labels the synthesized file path, so the real column was being shadowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
@tobby168
tobby168 marked this pull request as ready for review July 31, 2026 08:05
@tobby168
tobby168 requested a review from a team as a code owner July 31, 2026 08:05

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a DataSourceV2 implementation for Delta Lake tables, enabling log-driven file listing and pruning (both partition pruning and statistics-based skipping) via DeltaFileIndexer and prune_add_actions. It also adds logical rules (PushdownDeltaFilePruning) to push down predicates settled by PredicatePushdown to the DeltaFileIndexer, and updates read_delta to route through the V2 path when use_datasource_v2 is enabled. The review feedback highlights several instances of redundant wrapping of PyArrow objects (such as pa.schema() and pa.table()) where the underlying methods already return the correct types.

columns keep their declared position and type rather than being
appended as the strings the directory names encode.
"""
schema = pa.schema(self._open_table().schema().to_arrow())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to pa.schema() is redundant here because self._open_table().schema().to_arrow() already returns a pyarrow.Schema object. Removing the redundant wrapping simplifies the code and avoids unnecessary object instantiation.

Suggested change
schema = pa.schema(self._open_table().schema().to_arrow())
schema = self._open_table().schema().to_arrow()

version=self._version,
storage_options=self._storage_options,
)
add_actions = pa.table(table.get_add_actions(flatten=True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to pa.table() is redundant here because table.get_add_actions(flatten=True) already returns a pyarrow.Table object. Removing the redundant wrapping simplifies the code and avoids unnecessary object instantiation.

Suggested change
add_actions = pa.table(table.get_add_actions(flatten=True))
add_actions = table.get_add_actions(flatten=True)

if mode is not None and mode.lower() != "none":
return False

schema = pa.schema(dt.schema().to_arrow())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to pa.schema() is redundant here because dt.schema().to_arrow() already returns a pyarrow.Schema object. Removing the redundant wrapping simplifies the code and avoids unnecessary object instantiation.

Suggested change
schema = pa.schema(dt.schema().to_arrow())
schema = dt.schema().to_arrow()

if not partition_columns:
return {}

schema = pa.schema(dt.schema().to_arrow())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to pa.schema() is redundant here because dt.schema().to_arrow() already returns a pyarrow.Schema object. Removing the redundant wrapping simplifies the code and avoids unnecessary object instantiation.

Suggested change
schema = pa.schema(dt.schema().to_arrow())
schema = dt.schema().to_arrow()

INCLUDE_PATHS_COLUMN_NAME,
)

schema = pyarrow.schema(dt.schema().to_arrow())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to pyarrow.schema() is redundant here because dt.schema().to_arrow() already returns a pyarrow.Schema object. Removing the redundant wrapping simplifies the code and avoids unnecessary object instantiation.

Suggested change
schema = pyarrow.schema(dt.schema().to_arrow())
schema = dt.schema().to_arrow()

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 07c8fb0. Configure here.

Comment thread python/ray/data/read_api.py
split.partition_predicate
if residual_predicate is None
else residual_predicate & split.partition_predicate
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Residual filter needs partition columns

Medium Severity

When enforces_partition_predicate is false, the partition predicate is kept as a residual Filter above ReadFiles. That filter needs the partition columns in each block, but a columns projection can omit them so the reader never synthesizes them. The residual filter then fails at execution for patterns like reading a subset of columns and filtering on a partition key.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 07c8fb0. Configure here.

`_empty_delta_dataset` appended the synthesized `path` column and then
projected to `columns`, which dropped it again. A real read adds `path`
after the projection instead, so `read_delta(columns=["a"],
include_paths=True)` returned `["a", "path"]` for a populated table and
`["a"]` for an empty one -- a union of the two then failed on mismatched
schemas.

Both paths now derive their output columns from `_delta_output_columns`,
so they can't drift apart again, and the test covers each combination of
`columns` and `include_paths` rather than one option at a time.

Reported by Cursor Bugbot on ray-project#65142.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tobby168 <tobby168@gmail.com>
@tobby168

Copy link
Copy Markdown
Contributor Author

Thanks both — responses below, plus one behavior change worth flagging that came out of digging into the second Bugbot finding.

@gemini-code-assist: the five pa.schema() / pa.table() suggestions

These don't hold — deltalake returns arro3 objects, not PyArrow ones:

>>> import deltalake, pyarrow as pa
>>> deltalake.__version__
'1.5.0'
>>> type(dt.schema().to_arrow())
<class 'arro3.core._core.Schema'>
>>> isinstance(dt.schema().to_arrow(), pa.Schema)
False
>>> type(dt.get_add_actions(flatten=True))
<class 'arro3.core._core.Table'>
>>> isinstance(dt.get_add_actions(flatten=True), pa.Table)
False

The pa.schema(...) / pa.table(...) calls are genuine cross-library conversions over the Arrow PyCapsule interface, not redundant wrapping.

They're easy to mistake for no-ops because arro3 mirrors much of the PyArrow API — .names, .get_field_index() and .num_rows all work on the unwrapped objects. But the operations this code depends on don't:

>>> dt.get_add_actions(flatten=True).filter(mask)
AttributeError: 'arro3.core._core.Table' object has no attribute 'filter'

prune_add_actions filters the add-actions table, so dropping that conversion breaks file pruning outright, and arro3.Schema.empty_table() returns an arro3 table that from_arrow won't accept. Leaving all five as-is.

@cursor: "Empty read drops path column"

Correct, and fixed in 44ace4c. read_delta(columns=["a"], include_paths=True) returned ["a", "path"] for a populated table and ["a"] for an empty one, so a union of the two failed.

Both paths now derive their output columns from one helper, and the test covers each combination of columns and include_paths rather than one option at a time — the gap was that both options were tested, just never together.

@cursor: "Residual filter needs partition columns"

The described scenario doesn't reproduce: ProjectionPushdown already accounts for the columns a residual Filter binds to.

read_delta(t).filter(col("region") == lit("US")).select_columns(["val"])
  Project
    Filter  region == 'US'
      ReadFiles  scanner.columns=('region', 'val')   <-- region retained
        ListFiles
  -> [{'val': 1}]

But it pointed at something real. When columns= is passed to read_delta itself and excludes a partition column, filtering on that column afterwards now raises instead of silently working:

read_delta(t, columns=["val"]).filter(expr=col("region") == lit("US"))
# before: [{'val': 1}]
# now:    KeyError: Field "region" does not exist in schema

That is a consequence of this PR moving partition-predicate enforcement into a Filter. Previously the predicate was absorbed by path-based pruning, so nothing ever needed region in the block.

I've left it raising, for three reasons:

  1. The query filters on a column the caller projected away — the dataset's schema genuinely doesn't contain it.
  2. read_parquet raises on exactly this pattern today (RuntimeError: Filter expression '(region == "US")' failed on parquet file ... with columns: {'val'}), so raising is the established behavior; read_delta accepting it was the outlier.
  3. Both ways I tried to preserve the old behavior — adding partition columns to the scanner projection, and re-narrowing after the read — can't work, because the caller's .filter() is attached after read_delta has already returned.

Flagging it explicitly rather than leaving it to be discovered. Happy to reconsider if maintainers would rather keep the old leniency.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the detailed breakdown, @tobby168. Your analysis of the arro3 vs pyarrow object distinction is spot on, and it makes perfect sense why those conversions are necessary for the operations you're performing.

Regarding the behavior change for partition columns in read_delta when using columns= to filter them out: your reasoning for letting it raise is sound. Aligning with read_parquet's established behavior and treating the schema as the source of truth is the correct approach for maintaining consistency. Flagging this explicitly is the right call, as it prevents silent failures and makes the API contract clearer for users.

@jackylee-ch

Copy link
Copy Markdown

Drive-by note, not a review — this PR touches the same loop I was about to send a separate fix for, and they're different defects, so flagging here rather than opening a competing PR.

Your if value is None: continue handles a null partition value coming from null_fallback. The case I hit is a missing key: partitions[field] itself raises KeyError whenever a field_types key isn't present in the path — e.g. an unpartitioned file directly under base_dir, which Partitioning's own field comment (partitioning.py:93-96) documents as supported. It also breaks the recipe in PathPartitionFilter's docstring (:470, "Unpartitioned files are denoted with an empty input dictionary"):

f = PathPartitionFilter.of(lambda d: True if d else False, style="hive",
                           base_dir="/tmp/part_probe", field_types={"year": int})
f(["/tmp/part_probe/year=2024/f.csv", "/tmp/part_probe/loose.csv"])
# KeyError: 'year'

Either works for me: I can wait for this to land and send the missing-key fix on top, or you could fold it in here since it's the same two lines. Whichever you prefer.

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

Labels

community-contribution Contributed by the community data Ray Data-related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Data] Support predicate pushdown at Delta Lake level

2 participants