Skip to content

fix(query): enforce Exact filters on staging/listing scans and only report Exact for time-partition bounds - #65

Merged
VibeSWE merged 2 commits into
mainfrom
fix/exact-filter-enforcement
Oct 24, 2026
Merged

VibeSWE merged 2 commits into
mainfrom
fix/exact-filter-enforcement

Conversation

@VibeSWE

@VibeSWE VibeSWE commented Oct 24, 2026 •

Copy link
Copy Markdown

Problem

StandardTableProvider::supports_filters_pushdown reports a filter as Exact whenever expr_in_boundary accepts it. That means any <col> {<,<=,>,>=} <minute-aligned timestamp literal>, on any column. DataFusion then removes the FilterExec above the scan, and the scan is trusted to enforce the filter. Two places broke that trust:

  1. Unfiltered rows from in-memory staging and legacy listing. Parquet scans enforce Exact filters through the source predicate (exact_source_filters with pushdown_filters = true). But in DataFusion 55, MemTable::scan (in-memory staging records) ignores filters, and ListingTable::scan (legacy pre-manifest listing) never installs a predicate for them. Rows that should be filtered out came back. Reproduced:

    created_at >= '2020-01-01T00:00:00'               -> [0..7]  (expected [4,5,6,7])
    p_timestamp < '2026-10-17T12:30:00'               -> [0..7]  (expected [0,1,2,3])
    p_timestamp >= '2026-10-17T12:30:00' AND id < 6   -> [0..5]  (expected [4,5])
    listing: created_at >= '2020-01-01' LIMIT 2       -> [0, 1]  (non-matching rows)
    
  2. LIMIT returns too few rows. With no FilterExec left, DataFusion pushes LIMIT into the scan. collect_from_snapshot then truncates the file list once the files' num_rows reach the limit, assuming every row of a surviving file matches. That holds only for the time-partition column, because each file holds one minute of it. For WHERE created_at >= TIMESTAMP '2026-10-01 00:00:00' LIMIT 100, the list can be cut to a file with only 5 matching rows. The same happens for p_timestamp bounds on a stream with a custom time_partition, whose files are bucketed by event time.

Fix

  • scan_enforcing_exact_filters is now used for the MemTable staging scan and the legacy ListingTable scan. When a filter is exact-eligible, it scans the projected columns plus the predicate's columns with no limit. It then applies the conjunction in a FilterExec, which projects back to the requested columns and applies the limit after filtering. With no such filter, the scan is unchanged.
  • filter_pushdown: a filter is Exact only if its left side is a bare column equal to the stream's time-partition column (time_partition, else p_timestamp) and expr_in_boundary holds. Everything else is Inexact, so DataFusion keeps the FilterExec and does not push LIMIT down. StandardTableProvider now carries the stream's time_partition, taken from the same stream metadata that query::transform uses.

exact_source_filters (the parquet source predicate and the staging/listing enforcement) still uses the broader expr_in_boundary set. It is a superset of the Exact filters, so every Exact filter is enforced, and the extra ones only add parquet pruning under a retained FilterExec.

Found while reviewing #63; it is independent of that PR. The fix for (1) came from a review subagent (Opus 5.5) that reproduced the leak. The fix for (2) came from a separate review finding (Opus 5.5), checked against collect_from_snapshot.

Tests

  • New stream_schema_provider/exact_filter_tests.rs exercises the real reversed_mem_table and a real ListingTableBuilder table over a local parquet file. It covers non-time columns, time bounds, mixed exact/inexact filters, projections that exclude filter columns, count(*), and LIMIT.
  • only_minute_aligned_partition_column_bounds_are_exact: unit cases for filter_pushdown, including p_timestamp on a time-partitioned stream.
  • limit_is_pushed_into_scan_only_for_exact_partition_bounds: a planned SQL ... LIMIT 5 gets limit=5 on the scan for a p_timestamp bound. For a created_at bound it gets a FilterExec and no scan limit instead.
  • guarded_hot_tier_source_preserves_provider_identity now also checks time_partition.

The new tests fail without the corresponding change. cargo fmt --check is clean. cargo test passes: 853 passed, 0 failed. cargo clippy --all-targets shows no warnings in the changed files (-D warnings is already red on main from unrelated files).

Notes

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

VibeSWE and others added 2 commits October 23, 2026 20:25
…ng scans

StandardTableProvider reports any `<expr> {<,<=,>,>=} <minute-aligned
timestamp literal>` filter as Exact, for any column, so DataFusion drops
the FilterExec above the scan. Parquet scans enforce these through the
source predicate with pushdown_filters, but MemTable (in-memory staging
records) and ListingTable (legacy listing) ignore pushed filters, so their
rows were returned unfiltered, e.g. `WHERE created_at >= '2020-01-01'` or
a minute-aligned p_timestamp bound inside the staging window. The listing
scan also applied LIMIT before any filtering.

Scan both through scan_enforcing_exact_filters, which scans the projected
plus filter columns, applies the Exact conjunction in a FilterExec, then
projects and applies the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
expr_in_boundary accepted a minute-aligned timestamp bound on any column,
so supports_filters_pushdown reported e.g. `created_at >= '...T00:00:00'`
as Exact. DataFusion then drops the FilterExec and pushes LIMIT into the
scan, and collect_from_snapshot truncates the file list once the files'
row counts reach the limit, assuming every row of a surviving file
matches. For any column other than the one data files are partitioned
by, that undercounts: `... LIMIT 100` could return 5 rows.

Report Exact only when the filter's left side is the stream's time
partition column (p_timestamp unless configured): data files hold one
minute of that column, so a minute-aligned bound keeps or prunes each
file whole. StandardTableProvider now carries the stream's time
partition for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@VibeSWE
VibeSWE merged commit cf46011 into main Oct 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant