Pushdown matrix

May 2, 2026 · View on GitHub

What each source pushes down to its backend, and what stays in the DataFusion engine. "Native" means the predicate / projection / limit travels in the protocol the source speaks — over the wire to Postgres, into the LDAP filter, into the GraphQL POST body, into the URL params for REST, into the parquet file metadata for row-group pruning, etc. Everything else is a post-scan operation DataFusion runs on the buffered RecordBatch.

The point of pushing down is correctness and speed: it shrinks the wire footprint and lets the backend's own indexes do the matching work.

Per source

Source= !=< <= > >=IS [NOT] NULLIN (…)LIMITProjectionAggregates
REST✅ via URL params (pushdown.filters)configurable per-column param✅ via pushdown.limit✅ via pushdown.projection.style: comma_separated
rows-file (jsonl/csv/syslog/regex/logfmt)✅ via L3 zone-map + bloom skip✅ zone-map✅ chunk-level early break✅ column subset only
Parquet✅ row-group pruning + late materialisepartial (counts via metadata)
Excel✅ predicate evaluated during calamine row stream✅ string-comparison semantics✅ early break✅ project at array build time
GraphQL✅ via pushdown.variables mapping (WHERE col = lit$variable)❌ (single = only today)❌ (depends on the GraphQL query body)n/a (the query body picks fields)
PostgreSQL✅ native WHERE col = literal over the wireSELECT col1, col2…❌ today (server-side count(*) is a planned 0.5 win)
LDAP / LDAPS✅ AND-merged into the table filter as (attr=val) / (!(attr=val))✅ via (attr<=val) / (attr>=val) (note: < and > synthesised as (&(<=v)(!(=v))))✅ via (!(attr=*)) / (attr=*)✅ as (|(attr=v1)(attr=v2)…)post-scan todayattrs requested in the search
Programmaticdepends on the provider's ProviderCapabilitiesdependsdependsdependsdepends

How "native" gets verified

Every source has at least one test that asserts the pushdown arrives at the backend in the expected shape:

SourceTest
RESTselect_count_star_against_rest_source (wiremock asserts query string)
rows-filedbfy-provider-rows-file zone-map + bloom unit tests (skip ratio measured)
Parquettests/integration_parquet.rs — TPC-H-lite, row-group pruning observed via correctness
Excelexcel_pushdown_filters_at_read_time — 5-row workbook, asserts 3 rows survive the predicate
GraphQLgraphql_pushdown_sends_variable_values_to_endpointbody_partial_json matches the literal
Postgrespostgres_filter_translation_emits_correct_sql (unit) + tests/integration_postgres.rs (real PG)
LDAPldap_filter_translation_* (unit) + tests/integration_ldap.rs (real OpenLDAP)
Programmaticdbfy-provider-static round-trip tests

What's not pushed down (today)

  • Cross-source joins: each side scans independently with its own pushdown; the JOIN is a DataFusion HashJoin / SortMergeJoin in the engine. This is correct but means a JOIN that filters one side heavily still over-reads the unfiltered side. The 0.5 milestone adds dynamic_filter semi-join propagation so a heavily-filtered Postgres scan can shrink the LDAP scan it joins to.
  • Aggregates: count(*), sum, avg always run in DataFusion. Postgres / Parquet metadata-level aggregate pushdown is on the roadmap.
  • Window functions: never pushed down. Always evaluated by the engine.
  • String functions / casts in WHERE: WHERE upper(name) = 'ALICE' falls back to post-scan filtering on every source.

Adding pushdown for a new operator

  1. Extend try_extract_pushed_filter in crates/dbfy-frontend-datafusion/src/lib.rs to recognise the new Expr shape and emit a PushedFilter describing it.
  2. For each source provider, decide whether to support it:
    • Postgres: extend PostgresTableProvider::translate_filter to emit the SQL.
    • LDAP: extend LdapTableProvider::translate_filter to emit the filter fragment.
    • Excel: extend excel_row_matches for in-process evaluation.
  3. Add a unit test on the translator (string equality on the emitted fragment) and an integration test against the real backend.
  4. Update this matrix.