Repository: rapidsai/cudf
Pull request #18545: Implement row group pruning with bloom filters in experimental PQ reader
Author: login: mhaseeb123
Labels: name: feature request
description: New feature or request
name: libcudf
description: Affects libcudf (C++/CUDA) code.
name: CMake
description: CMake build issue
name: 5 - Ready to Merge
description: Testing and reviews complete, ready to merge
name: cuIO
description: cuIO issue
name: non-breaking
description: Non-breaking change
Created: 2025-04-22T01:29:04Z | Merged: 2025-05-15T20:15:43Z

## Description
Contributes to #17896. Part of #18011.

This PR implements row group pruning with bloom filters in the experimental Parquet reader optimized for hybrid scan queries. Dictionary based row group pruning is still WIP in a separate branch and so this PR has empty definitions where needed.

Note: Unfortunately, we can't add any tests for this feature as we don't yet have capability of writing parquet files with bloom filters. However, the code that filters row groups with bloom filters is identical to already tested code at:  https://github.com/rapidsai/cudf/blob/branch-25.06/cpp/src/io/parquet/predicate_pushdown.cpp#L198-L240

## Checklist
- [x] I am familiar with the [Contributing Guidelines](https://github.com/rapidsai/cudf/blob/HEAD/CONTRIBUTING.md).
- [ ] New or existing tests cover these changes.
- [x] The documentation is up to date with these changes.



Diff:
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 58f2b43303f..607df3d677e 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -546,6 +546,7 @@ add_library(
   src/io/parquet/compact_protocol_reader.cpp
   src/io/parquet/compact_protocol_writer.cpp
   src/io/parquet/decode_preprocess.cu
+  src/io/parquet/experimental/dictionary_page_filter.cu
   src/io/parquet/experimental/hybrid_scan.cpp
   src/io/parquet/experimental/hybrid_scan_helpers.cpp
   src/io/parquet/experimental/hybrid_scan_impl.cpp
diff --git a/cpp/include/cudf/io/experimental/hybrid_scan.hpp b/cpp/include/cudf/io/experimental/hybrid_scan.hpp
index df6a3a79d7d..081f95384ff 100644
--- a/cpp/include/cudf/io/experimental/hybrid_scan.hpp
+++ b/cpp/include/cudf/io/experimental/hybrid_scan.hpp
@@ -339,12 +339,16 @@ class hybrid_scan_reader {
     rmm::cuda_stream_view stream) const;
 
   /**
-   * @brief Get byte ranges of bloom filters and dictionary pages (secondary filters) for
-   * further row group pruning
+   * @brief Get byte ranges of bloom filters and dictionary pages (secondary filters) for row group
+   *        pruning
+   *
+   * @note Device buffers for bloom filter byte ranges must be allocated using a 32 byte
+   *       aligned memory resource
    *
    * @param row_group_indices Input row groups indices
    * @param options Parquet reader options
-   * @return Pair of vectors of byte ranges to per-column-chunk bloom filters and dictionary pages
+   * @return Pair of vectors of byte ranges of column chunk with bloom filters and dictionary
+   *         pages subject to filter predicate
    */
   [[nodiscard]] std::pair<std::vector<byte_range_info>, std::vector<byte_range_info>>
   secondary_filters_byte_ranges(cudf::host_span<size_type const> row_group_indices,
@@ -353,7 +357,8 @@ class hybrid_scan_reader {
   /**
    * @brief Filter the row groups using column chunk dictionary pages
    *
-   * @param dictionary_page_data Device buffers containing per-column-chunk dictionary page data
+   * @param dictionary_page_data Device buffers containing dictionary page data of column chunks
+   *                             with (in)equality predicate
    * @param row_group_indices Input row groups indices
    * @param options Parquet reader options
    * @param stream CUDA stream used for device memory operations and kernel launches
@@ -368,7 +373,11 @@ class hybrid_scan_reader {
   /**
    * @brief Filter the row groups using column chunk bloom filters
    *
-   * @param bloom_filter_data Device buffers containing per-column-chunk bloom filter data
+   * @note The `bloom_filter_data` device buffers must be allocated using a 32
+   *       byte aligned memory resource
+   *
+   * @param bloom_filter_data Device buffers containing bloom filter data of column chunks with
+   *                          an equality predicate
    * @param row_group_indices Input row groups indices
    * @param options Parquet reader options
    * @param stream CUDA stream used for device memory operations and kernel launches
diff --git a/cpp/src/io/parquet/bloom_filter_reader.cu b/cpp/src/io/parquet/bloom_filter_reader.cu
index 4a52962c676..8252ffd5646 100644
--- a/cpp/src/io/parquet/bloom_filter_reader.cu
+++ b/cpp/src/io/parquet/bloom_filter_reader.cu
@@ -513,7 +513,7 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
   host_span<std::vector<ast::literal*> const> literals,
   size_type total_row_groups,
   host_span<data_type const> output_dtypes,
-  host_span<int const> equality_col_schemas,
+  host_span<int const> bloom_filter_col_schemas,
   std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
@@ -521,7 +521,7 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
   auto const num_input_columns = static_cast<cudf::size_type>(output_dtypes.size());
 
   // Get parquet types for the predicate columns
-  auto const parquet_types = get_parquet_types(input_row_group_indices, equality_col_schemas);
+  auto const parquet_types = get_parquet_types(input_row_group_indices, bloom_filter_col_schemas);
 
   // Create spans from bloom filter bitset buffers to use in cuco::bloom_filter_ref.
   std::vector<cudf::device_span<cuda::std::byte>> h_bloom_filter_spans;
@@ -542,7 +542,7 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
   bloom_filter_caster const bloom_filter_col{bloom_filter_spans,
                                              parquet_types,
                                              static_cast<size_t>(total_row_groups),
-                                             equality_col_schemas.size()};
+                                             bloom_filter_col_schemas.size()};
 
   // Converts bloom filter membership for equality predicate columns to a table
   // containing a column for each `col[i] == literal` predicate to be evaluated.
@@ -584,8 +584,6 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
                                             stream);
 }
 
-equality_literals_collector::equality_literals_collector() = default;
-
 equality_literals_collector::equality_literals_collector(ast::expression const& expr,
                                                          cudf::size_type num_input_columns)
   : _num_input_columns{num_input_columns}
@@ -604,7 +602,7 @@ std::reference_wrapper<ast::expression const> equality_literals_collector::visit
   ast::column_reference const& expr)
 {
   CUDF_EXPECTS(expr.get_table_source() == ast::table_reference::LEFT,
-               "BloomfilterAST supports only left table");
+               "DictionaryAST and BloomfilterAST support only left table");
   CUDF_EXPECTS(expr.get_column_index() < _num_input_columns,
                "Column index cannot be more than number of columns in the table");
   return expr;
@@ -613,7 +611,7 @@ std::reference_wrapper<ast::expression const> equality_literals_collector::visit
 std::reference_wrapper<ast::expression const> equality_literals_collector::visit(
   ast::column_name_reference const& expr)
 {
-  CUDF_FAIL("Column name reference is not supported in BloomfilterAST");
+  CUDF_FAIL("Column name reference is not supported in DictionaryAST and BloomfilterAST");
 }
 
 std::reference_wrapper<ast::expression const> equality_literals_collector::visit(
diff --git a/cpp/src/io/parquet/experimental/dictionary_page_filter.cu b/cpp/src/io/parquet/experimental/dictionary_page_filter.cu
new file mode 100644
index 00000000000..bd3134fabcb
--- /dev/null
+++ b/cpp/src/io/parquet/experimental/dictionary_page_filter.cu
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_collector::visit(
+  ast::operation const& expr)
+{
+  using cudf::ast::ast_operator;
+  auto const operands = expr.get_operands();
+  auto const op       = expr.get_operator();
+
+  if (auto* v = dynamic_cast<ast::column_reference const*>(&operands[0].get())) {
+    // First operand should be column reference, second should be literal.
+    CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2,
+                 "Only binary operations are supported on column reference");
+    auto const literal_ptr = dynamic_cast<ast::literal const*>(&operands[1].get());
+    CUDF_EXPECTS(literal_ptr != nullptr,
+                 "Second operand of binary operation with column reference must be a literal");
+    v->accept(*this);
+
+    // Push to the corresponding column's literals and operators list iff EQUAL or NOT_EQUAL
+    // operator is seen
+    if (op == ast_operator::EQUAL or op == ast::ast_operator::NOT_EQUAL) {
+      auto const col_idx = v->get_column_index();
+      _literals[col_idx].emplace_back(const_cast<ast::literal*>(literal_ptr));
+    }
+  } else {
+    // Visit the operands and ignore any output as we only want to collect literals
+    std::ignore = visit_operands(operands);
+  }
+
+  return expr;
+}
+
+}  // namespace cudf::io::parquet::experimental::detail
diff --git a/cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp b/cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
index 2e252f992c5..a3fa581a36e 100644
--- a/cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
+++ b/cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
@@ -40,10 +40,10 @@ using io::detail::inline_column_buffer;
 using parquet::detail::CompactProtocolReader;
 using parquet::detail::equality_literals_collector;
 using parquet::detail::input_column_info;
-using parquet::detail::row_group_info;
+using text::byte_range_info;
 
 namespace {
-
+// Construct a vector of all row group indices from the input vectors
 [[nodiscard]] auto all_row_group_indices(
   host_span<std::vector<cudf::size_type> const> row_group_indices)
 {
@@ -51,6 +51,21 @@ namespace {
                                                    row_group_indices.end());
 }
 
+// Compute total number of input row groups
+[[nodiscard]] cudf::size_type compute_total_row_groups(
+  host_span<std::vector<cudf::size_type> const> row_group_indices)
+{
+  auto const total_row_groups = std::accumulate(
+    row_group_indices.begin(), row_group_indices.end(), size_t{0}, [](auto sum, auto const& pfm) {
+      return sum + pfm.size();
+    });
+
+  // Check if we have less than 2B total row groups.
+  CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
+               "Total number of row groups exceed the cudf::size_type's limit");
+  return static_cast<cudf::size_type>(total_row_groups);
+}
+
 }  // namespace
 
 metadata::metadata(cudf::host_span<uint8_t const> footer_bytes)
@@ -249,40 +264,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(thrust::counting_iterator<cudf::size_type>(0),
+                                        output_column_schemas.begin());
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<thrust::tuple<cudf::size_type, cudf::size_type>> dictionary_col_schemas;
+  thrust::copy_if(thrust::host,
+                  iter,
+                  iter + output_column_schemas.size(),
+                  literals.begin(),
+                  std::back_inserter(dictionary_col_schemas),
+                  [](auto& dict_literals) { return not dict_literals.empty(); });
+
+  // No (in)equality literals found, return empty vector
+  if (dictionary_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_dictionary_columns = dictionary_col_schemas.size();
+  auto const num_chunks             = total_row_groups * num_dictionary_columns;
+
+  std::vector<byte_range_info> dictionary_page_bytes;
+  dictionary_page_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid dictionary page
+  auto have_dictionary_pages = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
+    [&](auto const src_index) {
+      // Get all row group indices in the data source
+      auto const& rg_indices = row_group_indices[src_index];
+      // For all row groups
+      std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+        auto const& rg = per_file_metadata[0].row_groups[rg_index];
+        // For all column chunks
+        std::for_each(
+          dictionary_col_schemas.begin(),
+          dictionary_col_schemas.end(),
+          [&](auto const& schema_col_idx_pair) {
+            auto const [input_col_idx, schema_idx] = schema_col_idx_pair;
+            auto& col_meta        = get_column_metadata(rg_index, src_index, schema_idx);
+            auto const& col_chunk = rg.columns[input_col_idx];
+
+            auto dictionary_offset = int64_t{0};
+            auto dictionary_size   = int64_t{0};
+
+            // If any columns lack the page indexes then just return without modifying the
+            // row_group_info.
+            if (col_chunk.offset_index.has_value() and col_chunk.column_index.has_value()) {
+              auto const& offset_index = col_chunk.offset_index.value();
+              auto const num_pages     = offset_index.page_locations.size();
+
+              // There is a bug in older versions of parquet-mr where the first data page offset
+              // really points to the dictionary page. The first possible offset in a file is 4
+              // (after the "PAR1" header), so check to see if the dictionary_page_offset is > 0.
+              // If it is, then we haven't encountered the bug.
+              if (col_meta.dictionary_page_offset > 0) {
+                dictionary_offset     = col_meta.dictionary_page_offset;
+                dictionary_size       = col_meta.data_page_offset - dictionary_offset;
+                have_dictionary_pages = true;
+              } else {
+                // dictionary_page_offset is 0, so check to see if the data_page_offset does not
+                // match the first offset in the offset index.  If they don't match, then
+                // data_page_offset points to the dictionary page.
+                if (num_pages > 0 &&
+                    col_meta.data_page_offset < offset_index.page_locations[0].offset) {
+                  dictionary_offset = col_meta.data_page_offset;
+                  dictionary_size =
+                    offset_index.page_locations[0].offset - col_meta.data_page_offset;
+                  have_dictionary_pages = true;
+                }
+              }
+            }
+
+            dictionary_page_bytes.emplace_back(dictionary_offset, dictionary_size);
+          });
+      });
+    });
+
+  if (not have_dictionary_pages) { return {}; }
+
+  return dictionary_page_bytes;
+}
+
+std::vector<std::vector<cudf::size_type>>
+aggregate_reader_metadata::filter_row_groups_with_dictionary_pages(
+  cudf::detail::hostdevice_span<parquet::detail::ColumnChunkDesc const> chunks,
+  cudf::detail::hostdevice_span<parquet::detail::PageInfo const> pages,
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  cudf::host_span<std::vector<ast::literal*> const> literals,
+  cudf::host_span<data_type const> output_dtypes,
+  cudf::host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
+  rmm::cuda_stream_view stream) const
+{
+  // Not yet implemented so just return all row group indices
+  return all_row_group_indices(row_group_indices);
+}
+
+std::vector<std::vector<cudf::size_type>>
+aggregate_reader_metadata::filter_row_groups_with_bloom_filters(
+  cudf::host_span<rmm::device_buffer> bloom_filter_data,
+  host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
+  rmm::cuda_stream_view stream) const
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& eq_literals) { return not eq_literals.empty(); });
+
+  // Return all row groups if no column with equality predicate(s)
+  if (bloom_filter_col_schemas.empty()) { return all_row_group_indices(row_group_indices); }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  auto const bloom_filtered_row_groups = apply_bloom_filters(bloom_filter_data,
+                                                             row_group_indices,
+                                                             literals,
+                                                             total_row_groups,
+                                                             output_dtypes,
+                                                             bloom_filter_col_schemas,
+                                                             filter,
+                                                             stream);
+
+  return bloom_filtered_row_groups.value_or(all_row_group_indices(row_group_indices));
+}
+
 }  // namespace cudf::io::parquet::experimental::detail
diff --git a/cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp b/cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp
index 4e322ba0322..e490f6b1106 100644
--- a/cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp
+++ b/cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp
@@ -50,53 +50,6 @@ struct metadata : private metadata_base {
 };
 
 class aggregate_reader_metadata : public aggregate_reader_metadata_base {
- private:
-  /**
-   * @brief Materializes column chunk dictionary pages into `cuco::static_set`s
-   *
-   * @param dictionary_page_data Dictionary page data device buffers for each input row group
-   * @param input_row_group_indices Lists of input row groups, one per source
-   * @param total_row_groups Total number of row groups in `input_row_group_indices`
-   * @param output_dtypes Datatypes of output columns
-   * @param dictionary_col_schemas schema indices of dictionary columns only
-   * @param stream CUDA stream used for device memory operations and kernel launches
-   *
-   * @return A flattened list of `cuco::static_set_ref` device buffers for each filter column
-   * across row groups
-   */
-  [[nodiscard]] std::vector<rmm::device_buffer> materialize_dictionaries(
-    cudf::host_span<rmm::device_buffer> dictionary_page_data,
-    host_span<std::vector<size_type> const> input_row_group_indices,
-    host_span<data_type const> output_dtypes,
-    host_span<int const> dictionary_col_schemas,
-    rmm::cuda_stream_view stream) const;
-
-  /**
-   * @brief Filters the row groups using dictionary pages
-   *
-   * @param dictionaries `cuco::static_set_ref` device buffers for column chunk dictionary
-   * @param input_row_group_indices Lists of input row groups, one per source
-   * @param literals Lists of literals, one per input column
-   * @param operators Lists of operators, one per input column
-   * @param total_row_groups Total number of row groups in `input_row_group_indices`
-   * @param output_dtypes Datatypes of output columns
-   * @param dictionary_col_schemas schema indices of dictionary columns only
-   * @param filter AST expression to filter row groups based on bloom filter membership
-   * @param stream CUDA stream used for device memory operations and kernel launches
-   *
-   * @return Surviving row group indices if any of them are filtered.
-   */
-  [[nodiscard]] std::optional<std::vector<std::vector<size_type>>> apply_dictionary_filter(
-    cudf::host_span<rmm::device_buffer> dictionaries,
-    host_span<std::vector<size_type> const> input_row_group_indices,
-    host_span<std::vector<ast::literal*> const> literals,
-    host_span<std::vector<ast::ast_operator> const> operators,
-    size_type total_row_groups,
-    host_span<data_type const> output_dtypes,
-    host_span<int const> dictionary_col_schemas,
-    std::reference_wrapper<ast::expression const> filter,
-    rmm::cuda_stream_view stream) const;
-
  public:
   /**
    * @brief Constructor for aggregate_reader_metadata
@@ -161,17 +114,113 @@ class aggregate_reader_metadata : public aggregate_reader_metadata_base {
    * @param row_group_indices Input row groups indices
    * @param output_dtypes Datatypes of output columns
    * @param output_column_schemas schema indices of output columns
-   * @param filter Optional AST expression to filter row groups based on Column chunk statistics
+   * @param filter AST expression to filter row groups based on Column chunk statistics
    * @param stream CUDA stream used for device memory operations and kernel launches
    *
    * @return Filtered row group indices, if any are filtered
    */
   [[nodiscard]] std::vector<std::vector<size_type>> filter_row_groups_with_stats(
+    cudf::host_span<std::vector<size_type> const> row_group_indices,
+    cudf::host_span<data_type const> output_dtypes,
+    cudf::host_span<cudf::size_type const> output_column_schemas,
+    std::reference_wrapper<ast::expression const> filter,
+    rmm::cuda_stream_view stream) const;
+
+  /**
+   * @brief Get the bloom filter byte ranges, one per column chunk with equality predicate
+   *
+   * @param row_group_indices Input row groups indices
+   * @param output_dtypes Datatypes of output columns
+   * @param output_column_schemas schema indices of output columns
+   * @param filter AST expression to filter row groups based on bloom filters
+   *
+   * @return Byte ranges of bloom filters, one per column chunk with equality predicate
+   */
+  [[nodiscard]] std::vector<cudf::io::text::byte_range_info> get_bloom_filter_bytes(
+    cudf::host_span<std::vector<size_type> const> row_group_indices,
+    cudf::host_span<data_type const> output_dtypes,
+    cudf::host_span<cudf::size_type const> output_column_schemas,
+    std::reference_wrapper<ast::expression const> filter);
+
+  /**
+   * @brief Get the dictionary page byte ranges, one per column chunk with (in)equality predicate
+   *
+   * @param row_group_indices Input row groups indices
+   * @param output_dtypes Datatypes of output columns
+   * @param output_column_schemas schema indices of output columns
+   * @param filter AST expression to filter row groups based on dictionary pages
+   *
+   * @return Byte ranges of dictionary pages, one input column chunk with (in)equality predicate
+   */
+  [[nodiscard]] std::vector<cudf::io::text::byte_range_info> get_dictionary_page_bytes(
+    cudf::host_span<std::vector<size_type> const> row_group_indices,
+    cudf::host_span<data_type const> output_dtypes,
+    cudf::host_span<cudf::size_type const> output_column_schemas,
+    std::reference_wrapper<ast::expression const> filter);
+
+  /**
+   * @brief Filter the row groups using dictionaries based on predicate filter
+   *
+   * @param chunks Host device span of column chunk descriptors, one per column chunk with
+   *               dictionary page and (in)equality predicate
+   * @param pages Host device span of decoded page headers, one per column chunk with dictionary
+   *              page and (in)equality predicate
+   * @param row_group_indices Input row groups indices
+   * @param literals Lists of literals, one per input column
+   * @param output_dtypes Datatypes of output columns
+   * @param dictionary_col_schemas schema indices of dictionary columns only
+   * @param filter AST expression to filter row groups based on dictionary pages
+   * @param stream CUDA stream used for device memory operations and kernel launches
+   *
+   * @return Filtered row group indices, if any are filtered
+   */
+  [[nodiscard]] std::vector<std::vector<cudf::size_type>> filter_row_groups_with_dictionary_pages(
+    cudf::detail::hostdevice_span<parquet::detail::ColumnChunkDesc const> chunks,
+    cudf::detail::hostdevice_span<parquet::detail::PageInfo const> pages,
+    cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+    cudf::host_span<std::vector<ast::literal*> const> literals,
+    cudf::host_span<data_type const> output_dtypes,
+    cudf::host_span<cudf::size_type const> output_column_schemas,
+    std::reference_wrapper<ast::expression const> filter,
+    rmm::cuda_stream_view stream) const;
+
+  /**
+   * @brief Filter the row groups using bloom filters based on predicate filter
+   *
+   * @param bloom_filter_data Device buffers of bloom filters, one per input column chunk
+   * @param row_group_indices Input row groups indices
+   * @param output_dtypes Datatypes of output columns
+   * @param output_column_schemas schema indices of output columns
+   * @param filter AST expression to filter row groups based on bloom filters
+   * @param stream CUDA stream used for device memory operations and kernel launches
+   *
+   * @return Filtered row group indices, if any are filtered
+   */
+  [[nodiscard]] std::vector<std::vector<size_type>> filter_row_groups_with_bloom_filters(
+    cudf::host_span<rmm::device_buffer> bloom_filter_data,
     host_span<std::vector<size_type> const> row_group_indices,
     host_span<data_type const> output_dtypes,
-    host_span<int const> output_column_schemas,
-    std::optional<std::reference_wrapper<ast::expression const>> filter,
+    host_span<cudf::size_type const> output_column_schemas,
+    std::reference_wrapper<ast::expression const> filter,
     rmm::cuda_stream_view stream) const;
 };
 
+/**
+ * @brief Collects lists of equal and not-equal predicate literals in the AST expression, one list
+ * per input table column. This is used in row group filtering based on dictionary pages.
+ */
+class dictionary_literals_collector : public equality_literals_collector {
+ public:
+  dictionary_literals_collector() = default;
+
+  dictionary_literals_collector(ast::expression const& expr, cudf::size_type num_input_columns);
+
+  using equality_literals_collector::visit;
+
+  /**
+   * @copydoc ast::detail::expression_transformer::visit(ast::operation const& )
+   */
+  std::reference_wrapper<ast::expression const> visit(ast::operation const& expr) override;
+};
+
 }  // namespace cudf::io::parquet::experimental::detail
diff --git a/cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp b/cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp
index ca507d88959..5384e744fd7 100644
--- a/cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp
+++ b/cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp
@@ -178,7 +178,7 @@ std::vector<std::vector<size_type>> hybrid_scan_reader_impl::filter_row_groups_w
   return _metadata->filter_row_groups_with_stats(row_group_indices,
                                                  output_dtypes,
                                                  _output_column_schemas,
-                                                 expr_conv.get_converted_expr(),
+                                                 expr_conv.get_converted_expr().value(),
                                                  stream);
 }
 
@@ -187,7 +187,30 @@ hybrid_scan_reader_impl::secondary_filters_byte_ranges(
   cudf::host_span<std::vector<size_type> const> row_group_indices,
   parquet_reader_options const& options)
 {
-  return {};
+  CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
+  CUDF_EXPECTS(options.get_filter().has_value(), "Filter expression must not be empty");
+
+  select_columns(read_mode::FILTER_COLUMNS, options);
+
+  table_metadata metadata;
+  populate_metadata(metadata);
+  auto expr_conv = named_to_reference_converter(options.get_filter(), metadata);
+  CUDF_EXPECTS(expr_conv.get_converted_expr().has_value(),
+               "Columns names in filter expression must be convertible to index references");
+  auto output_dtypes = get_output_types(_output_buffers_template);
+
+  auto const bloom_filter_bytes =
+    _metadata->get_bloom_filter_bytes(row_group_indices,
+                                      output_dtypes,
+                                      _output_column_schemas,
+                                      expr_conv.get_converted_expr().value());
+  auto const dictionary_page_bytes =
+    _metadata->get_dictionary_page_bytes(row_group_indices,
+                                         output_dtypes,
+                                         _output_column_schemas,
+                                         expr_conv.get_converted_expr().value());
+
+  return {bloom_filter_bytes, dictionary_page_bytes};
 }
 
 std::vector<std::vector<size_type>>
@@ -197,7 +220,13 @@ hybrid_scan_reader_impl::filter_row_groups_with_dictionary_pages(
   parquet_reader_options const& options,
   rmm::cuda_stream_view stream)
 {
-  return {};
+  CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
+  CUDF_EXPECTS(options.get_filter().has_value(), "Encountered empty converted filter expression");
+
+  select_columns(read_mode::FILTER_COLUMNS, options);
+
+  return _metadata->filter_row_groups_with_dictionary_pages(
+    {}, {}, row_group_indices, {}, {}, {}, options.get_filter().value(), stream);
 }
 
 std::vector<std::vector<size_type>> hybrid_scan_reader_impl::filter_row_groups_with_bloom_filters(
@@ -206,7 +235,24 @@ std::vector<std::vector<size_type>> hybrid_scan_reader_impl::filter_row_groups_w
   parquet_reader_options const& options,
   rmm::cuda_stream_view stream)
 {
-  return {};
+  CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
+  CUDF_EXPECTS(options.get_filter().has_value(), "Encountered empty converted filter expression");
+
+  select_columns(read_mode::FILTER_COLUMNS, options);
+
+  table_metadata metadata;
+  populate_metadata(metadata);
+  auto expr_conv = named_to_reference_converter(options.get_filter(), metadata);
+  CUDF_EXPECTS(expr_conv.get_converted_expr().has_value(),
+               "Columns names in filter expression must be convertible to index references");
+  auto output_dtypes = get_output_types(_output_buffers_template);
+
+  return _metadata->filter_row_groups_with_bloom_filters(bloom_filter_data,
+                                                         row_group_indices,
+                                                         output_dtypes,
+                                                         _output_column_schemas,
+                                                         expr_conv.get_converted_expr().value(),
+                                                         stream);
 }
 
 std::pair<std::unique_ptr<cudf::column>, std::vector<thrust::host_vector<bool>>>
diff --git a/cpp/src/io/parquet/reader_impl_helpers.hpp b/cpp/src/io/parquet/reader_impl_helpers.hpp
index 935abbaab83..1fb7081d0d9 100644
--- a/cpp/src/io/parquet/reader_impl_helpers.hpp
+++ b/cpp/src/io/parquet/reader_impl_helpers.hpp
@@ -272,7 +272,7 @@ class aggregate_reader_metadata {
    * @param literals Lists of equality literals, one per each input row group
    * @param total_row_groups Total number of row groups in `input_row_group_indices`
    * @param output_dtypes Datatypes of output columns
-   * @param equality_col_schemas schema indices of equality columns only
+   * @param bloom_filter_col_schemas Schema indices of bloom filter columns only
    * @param filter AST expression to filter row groups based on bloom filter membership
    * @param stream CUDA stream used for device memory operations and kernel launches
    *
@@ -284,7 +284,7 @@ class aggregate_reader_metadata {
     host_span<std::vector<ast::literal*> const> literals,
     size_type total_row_groups,
     host_span<data_type const> output_dtypes,
-    host_span<int const> equality_col_schemas,
+    host_span<int const> bloom_filter_col_schemas,
     std::reference_wrapper<ast::expression const> filter,
     rmm::cuda_stream_view stream) const;
 
@@ -574,7 +574,7 @@ class named_to_reference_converter : public ast::detail::expression_transformer
  */
 class equality_literals_collector : public ast::detail::expression_transformer {
  public:
-  equality_literals_collector();
+  equality_literals_collector() = default;
 
   equality_literals_collector(ast::expression const& expr, cudf::size_type num_input_columns);
 


Reviews:
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T18:48:27Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T20:50:06Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T22:15:33Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T01:28:26Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T23:57:21Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T00:07:29Z
user: login: vuule
body: partial review
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:53:51Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:16:00Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:56:37Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T02:57:51Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:01:57Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T03:03:06Z
user: login: vuule
body: couple of small suggestion, nothing blocking
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T20:31:36Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T22:08:47Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T22:33:59Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T21:36:09Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T21:36:26Z
user: login: lamarrr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:42:28Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T02:46:45Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T03:21:58Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T03:24:30Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T03:26:15Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T03:26:24Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T03:28:37Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T03:52:56Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T03:58:39Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:39:28Z
user: login: ttnghia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:41:29Z
user: login: ttnghia
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:42:26Z
user: login: mhaseeb123
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T17:58:07Z
user: login: bdice
body: CMake approval.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:13:49Z

Review comments:
diff_hunk: @@ -231,26 +247,8 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   std::optional<std::reference_wrapper<ast::expression const>> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: No need for this as we check the converted expression in `hybrid_scan_impl.cpp` everytime.
created_at: 2025-05-06T18:48:27Z
author_association: MEMBER
diff_hunk: @@ -44,13 +44,29 @@ using parquet::detail::row_group_info;
 
 namespace {
 
+// Construct a vector of all row group indices from the input vectors
 [[nodiscard]] auto all_row_group_indices(
   host_span<std::vector<cudf::size_type> const> row_group_indices)
 {
   return std::vector<std::vector<cudf::size_type>>(row_group_indices.begin(),
                                                    row_group_indices.end());
 }
 
+// Compute total number of input row groups
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: Move this out as being used at multiple places
created_at: 2025-05-06T20:50:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_and_operators_collector::dictionary_literals_and_operators_collector() =
+  default;
+
+dictionary_literals_and_operators_collector::dictionary_literals_and_operators_collector(
+  ast::expression const& expr, cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  _operators.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_and_operators_collector::visit(
+  ast::column_reference const& expr)
+{
+  CUDF_EXPECTS(expr.get_table_source() == ast::table_reference::LEFT,
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: This is all identical to https://github.com/rapidsai/cudf/blob/branch-25.06/cpp/src/io/parquet/bloom_filter_reader.cu#L589 

created_at: 2025-05-06T21:52:06Z
author_association: MEMBER
diff_hunk: @@ -191,7 +208,13 @@ hybrid_scan_reader_impl::filter_row_groups_with_dictionary_pages(
   parquet_reader_options const& options,
   rmm::cuda_stream_view stream)
 {
-  return {};
+  CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
+  CUDF_EXPECTS(options.get_filter().has_value(), "Encountered empty converted filter expression");
+
+  select_columns(read_mode::FILTER_COLUMNS, options);
+
+  return _metadata->filter_row_groups_with_dictionary_pages(
path: cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp
user: login: mhaseeb123
body: Call with empty args as it will just return `all_row_group_indices()` for now
created_at: 2025-05-07T01:28:25Z
author_association: MEMBER
diff_hunk: @@ -263,4 +260,231 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const equality_literals =
+    equality_literals_collector{filter.value().get(),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: so `filter` is not really optional?
created_at: 2025-05-08T20:51:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -263,4 +260,231 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const equality_literals =
+    equality_literals_collector{filter.value().get(),
+                                static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> equality_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  equality_literals.begin(),
+                  std::back_inserter(equality_col_schemas),
+                  [](auto& eq_literals) { return not eq_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (equality_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_equality_columns = equality_col_schemas.size();
+  auto const num_chunks           = total_row_groups * num_equality_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
+    [&](auto const src_index) {
+      // Get all row group indices in the data source
+      auto const& rg_indices = row_group_indices[src_index];
+      // For all row groups
+      std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+        // For all column chunks
+        std::for_each(
+          equality_col_schemas.begin(), equality_col_schemas.end(), [&](auto const schema_idx) {
+            auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+            // Get bloom filter offsets and sizes
+            bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                            col_meta.bloom_filter_length.value_or(0));
+
+            // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+            if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+          });
+      });
+    });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const [literals, operators] =
+    dictionary_literals_and_operators_collector{filter.value().get(),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: same here
created_at: 2025-05-08T20:51:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector() = default;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_collector::visit(
+  ast::operation const& expr)
+{
+  using cudf::ast::ast_operator;
+  auto const operands = expr.get_operands();
+  auto const op       = expr.get_operator();
+
+  if (auto* v = dynamic_cast<ast::column_reference const*>(&operands[0].get())) {
+    // First operand should be column reference, second should be literal.
+    CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2,
+                 "Only binary operations are supported on column reference");
+    auto const literal_ptr = dynamic_cast<ast::literal const*>(&operands[1].get());
+    CUDF_EXPECTS(literal_ptr != nullptr,
+                 "Second operand of binary operation with column reference must be a literal");
+    v->accept(*this);
+
+    // Push to the corresponding column's literals and operators list iff EQUAL or NOT_EQUAL
+    // operator is seen
+    if (op == ast_operator::EQUAL or op == ast::ast_operator::NOT_EQUAL) {
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: Everything here is copy pasted from `equality_literals_collector::visit` except it collects if `op == EQUAL or op == NOT_EQUAL` instead of just `op == EQUAL`
created_at: 2025-05-08T23:57:20Z
author_association: MEMBER
diff_hunk: @@ -50,53 +50,6 @@ struct metadata : private metadata_base {
 };
 
 class aggregate_reader_metadata : public aggregate_reader_metadata_base {
- private:
-  /**
-   * @brief Materializes column chunk dictionary pages into `cuco::static_set`s
-   *
-   * @param dictionary_page_data Dictionary page data device buffers for each input row group
-   * @param input_row_group_indices Lists of input row groups, one per source
-   * @param total_row_groups Total number of row groups in `input_row_group_indices`
-   * @param output_dtypes Datatypes of output columns
-   * @param dictionary_col_schemas schema indices of dictionary columns only
-   * @param stream CUDA stream used for device memory operations and kernel launches
-   *
-   * @return A flattened list of `cuco::static_set_ref` device buffers for each filter column
-   * across row groups
-   */
-  [[nodiscard]] std::vector<rmm::device_buffer> materialize_dictionaries(
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.hpp
user: login: mhaseeb123
body: Removed these two function declarations for now since their signature is changed in an upcoming PR. The same PR will add their definition as well.
created_at: 2025-05-09T00:07:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector() = default;
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: vuule
body: does defaulting in the source file have a different effect compared to defaulting in the class declaration? I feel like this is the first time I've seen a default implementation in a source file.
created_at: 2025-05-09T01:23:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector() = default;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_collector::visit(
+  ast::operation const& expr)
+{
+  using cudf::ast::ast_operator;
+  auto const operands = expr.get_operands();
+  auto const op       = expr.get_operator();
+
+  if (auto* v = dynamic_cast<ast::column_reference const*>(&operands[0].get())) {
+    // First operand should be column reference, second should be literal.
+    CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2,
+                 "Only binary operations are supported on column reference");
+    auto const literal_ptr = dynamic_cast<ast::literal const*>(&operands[1].get());
+    CUDF_EXPECTS(literal_ptr != nullptr,
+                 "Second operand of binary operation with column reference must be a literal");
+    v->accept(*this);
+
+    // Push to the corresponding column's literals and operators list iff EQUAL or NOT_EQUAL
+    // operator is seen
+    if (op == ast_operator::EQUAL or op == ast::ast_operator::NOT_EQUAL) {
+      auto const col_idx = v->get_column_index();
+      _literals[col_idx].emplace_back(const_cast<ast::literal*>(literal_ptr));
+    }
+  } else {
+    // Just visit the operands and ignore any output
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: vuule
body: nitpick, but the code is as easy to read as the comment. IMO it would be more useful for the comment to explain why the output is ignored.
created_at: 2025-05-09T01:38:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +40,32 @@ using io::detail::inline_column_buffer;
 using parquet::detail::CompactProtocolReader;
 using parquet::detail::equality_literals_collector;
 using parquet::detail::input_column_info;
-using parquet::detail::row_group_info;
+using text::byte_range_info;
 
 namespace {
-
+// Construct a vector of all row group indices from the input vectors
 [[nodiscard]] auto all_row_group_indices(
   host_span<std::vector<cudf::size_type> const> row_group_indices)
 {
   return std::vector<std::vector<cudf::size_type>>(row_group_indices.begin(),
                                                    row_group_indices.end());
 }
 
+// Compute total number of input row groups
+[[nodiscard]] cudf::size_type compute_total_row_groups(
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: is this the same as https://github.com/rapidsai/cudf/pull/18716?
created_at: 2025-05-09T01:41:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +40,32 @@ using io::detail::inline_column_buffer;
 using parquet::detail::CompactProtocolReader;
 using parquet::detail::equality_literals_collector;
 using parquet::detail::input_column_info;
-using parquet::detail::row_group_info;
+using text::byte_range_info;
 
 namespace {
-
+// Construct a vector of all row group indices from the input vectors
 [[nodiscard]] auto all_row_group_indices(
   host_span<std::vector<cudf::size_type> const> row_group_indices)
 {
   return std::vector<std::vector<cudf::size_type>>(row_group_indices.begin(),
                                                    row_group_indices.end());
 }
 
+// Compute total number of input row groups
+[[nodiscard]] cudf::size_type compute_total_row_groups(
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: Oh no, this computes the total number of `row groups` (basically the number of row group indices) in the input span of row group indices. #18716 computes the number of `rows` in the input span of row group indices.
created_at: 2025-05-09T02:16:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector() = default;
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: Nope, it's all the same I think. Updated
created_at: 2025-05-09T02:56:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector() = default;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_collector::visit(
+  ast::operation const& expr)
+{
+  using cudf::ast::ast_operator;
+  auto const operands = expr.get_operands();
+  auto const op       = expr.get_operator();
+
+  if (auto* v = dynamic_cast<ast::column_reference const*>(&operands[0].get())) {
+    // First operand should be column reference, second should be literal.
+    CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2,
+                 "Only binary operations are supported on column reference");
+    auto const literal_ptr = dynamic_cast<ast::literal const*>(&operands[1].get());
+    CUDF_EXPECTS(literal_ptr != nullptr,
+                 "Second operand of binary operation with column reference must be a literal");
+    v->accept(*this);
+
+    // Push to the corresponding column's literals and operators list iff EQUAL or NOT_EQUAL
+    // operator is seen
+    if (op == ast_operator::EQUAL or op == ast::ast_operator::NOT_EQUAL) {
+      auto const col_idx = v->get_column_index();
+      _literals[col_idx].emplace_back(const_cast<ast::literal*>(literal_ptr));
+    }
+  } else {
+    // Just visit the operands and ignore any output
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: Oh it's because we only want to collect the literals we are interested in and don't care about anything else otherwise at this stage. Updated the comment
created_at: 2025-05-09T02:57:50Z
author_association: MEMBER
diff_hunk: @@ -263,4 +260,231 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const equality_literals =
+    equality_literals_collector{filter.value().get(),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: No as we want to only collect bloom filter byte ranges for columns that can participate in rg pruning (with bloom filters) based on the filter expression. 
created_at: 2025-05-09T03:01:55Z
author_association: MEMBER
diff_hunk: @@ -263,4 +260,231 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const equality_literals =
+    equality_literals_collector{filter.value().get(),
+                                static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> equality_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  equality_literals.begin(),
+                  std::back_inserter(equality_col_schemas),
+                  [](auto& eq_literals) { return not eq_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (equality_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_equality_columns = equality_col_schemas.size();
+  auto const num_chunks           = total_row_groups * num_equality_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
+    [&](auto const src_index) {
+      // Get all row group indices in the data source
+      auto const& rg_indices = row_group_indices[src_index];
+      // For all row groups
+      std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+        // For all column chunks
+        std::for_each(
+          equality_col_schemas.begin(), equality_col_schemas.end(), [&](auto const schema_idx) {
+            auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+            // Get bloom filter offsets and sizes
+            bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                            col_meta.bloom_filter_length.value_or(0));
+
+            // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+            if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+          });
+      });
+    });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::optional<std::reference_wrapper<ast::expression const>> filter)
+{
+  // Collect equality literals for each input table column
+  auto const [literals, operators] =
+    dictionary_literals_and_operators_collector{filter.value().get(),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: Not optional at this point. Updated the input arg type too. We are checking if `filter.has_value()` in `hybrid_scan_impl.cpp` before calling this and any other filter function essentially
created_at: 2025-05-09T03:03:06Z
author_association: MEMBER
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: `std::copy_if`?
created_at: 2025-05-09T20:17:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(
+    thrust::make_tuple(thrust::counting_iterator(0), output_column_schemas.begin()));
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: should we be explicit about the type of the counter here? AFAICT we do need to exactly match the type of `dictionary_col_schemas`
created_at: 2025-05-09T20:25:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(
+    thrust::make_tuple(thrust::counting_iterator(0), output_column_schemas.begin()));
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<thrust::tuple<cudf::size_type, cudf::size_type>> dictionary_col_schemas;
+  thrust::copy_if(thrust::host,
+                  iter,
+                  iter + output_column_schemas.size(),
+                  literals.begin(),
+                  std::back_inserter(dictionary_col_schemas),
+                  [](auto& dict_literals) { return not dict_literals.empty(); });
+
+  // No (in)equality literals found, return empty vector
+  if (dictionary_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_dictionary_columns = dictionary_col_schemas.size();
+  auto const num_chunks             = total_row_groups * num_dictionary_columns;
+
+  std::vector<byte_range_info> dictionary_page_bytes;
+  dictionary_page_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid dictionary page
+  auto have_dictionary_pages = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
+    [&](auto const src_index) {
+      // Get all row group indices in the data source
+      auto const& rg_indices = row_group_indices[src_index];
+      // For all row groups
+      std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+        auto const& rg = per_file_metadata[0].row_groups[rg_index];
+        // For all column chunks
+        std::for_each(
+          dictionary_col_schemas.begin(),
+          dictionary_col_schemas.end(),
+          [&](auto const& schema_col_idx_pair) {
+            auto const input_col_idx = thrust::get<0>(schema_col_idx_pair);
+            auto const schema_idx    = thrust::get<1>(schema_col_idx_pair);
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: vuule
body: does this work?
```suggestion
            auto const [input_col_idx, schema_idx] = schema_col_idx_pair;
```
created_at: 2025-05-09T20:27:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: We need the `thrust` version to use the stencil on `literals`.
created_at: 2025-05-09T22:08:47Z
author_association: MEMBER
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(
+    thrust::make_tuple(thrust::counting_iterator(0), output_column_schemas.begin()));
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: Good suggestion, done. Also cleaned up mixed use of `int` and `cudf::size_type` to all `cudf::size_type`
created_at: 2025-05-09T22:33:58Z
author_association: MEMBER
diff_hunk: @@ -227,40 +242,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(thrust::make_tuple(
+    thrust::counting_iterator<cudf::size_type>(0), output_column_schemas.begin()));
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<thrust::tuple<cudf::size_type, cudf::size_type>> dictionary_col_schemas;
+  thrust::copy_if(thrust::host,
+                  iter,
+                  iter + output_column_schemas.size(),
+                  literals.begin(),
+                  std::back_inserter(dictionary_col_schemas),
+                  [](auto& dict_literals) { return not dict_literals.empty(); });
+
+  // No (in)equality literals found, return empty vector
+  if (dictionary_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_dictionary_columns = dictionary_col_schemas.size();
+  auto const num_chunks             = total_row_groups * num_dictionary_columns;
+
+  std::vector<byte_range_info> dictionary_page_bytes;
+  dictionary_page_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid dictionary page
+  auto have_dictionary_pages = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
+    [&](auto const src_index) {
+      // Get all row group indices in the data source
+      auto const& rg_indices = row_group_indices[src_index];
+      // For all row groups
+      std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+        auto const& rg = per_file_metadata[0].row_groups[rg_index];
+        // For all column chunks
+        std::for_each(
+          dictionary_col_schemas.begin(),
+          dictionary_col_schemas.end(),
+          [&](auto const& schema_col_idx_pair) {
+            auto const [input_col_idx, schema_idx] = schema_col_idx_pair;
+            auto& col_meta        = get_column_metadata(rg_index, src_index, schema_idx);
+            auto const& col_chunk = rg.columns[input_col_idx];
+
+            auto dictionary_offset = int64_t{0};
+            auto dictionary_size   = int64_t{0};
+
+            // If any columns lack the page indexes then just return without modifying the
+            // row_group_info.
+            if (col_chunk.offset_index.has_value() and col_chunk.column_index.has_value()) {
+              auto const& offset_index = col_chunk.offset_index.value();
+              auto const num_pages     = offset_index.page_locations.size();
+
+              // There is a bug in older versions of parquet-mr where the first data page offset
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: This segment of code taken from `reader_impl_helpers.cpp` as is
created_at: 2025-05-13T21:36:09Z
author_association: MEMBER
diff_hunk: @@ -227,40 +242,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: This segment of code taken from `bloom_filter_reader.cu` as is
created_at: 2025-05-13T21:36:26Z
author_association: MEMBER
diff_hunk: @@ -584,8 +584,6 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
                                             stream);
 }
 
-equality_literals_collector::equality_literals_collector() = default;
path: cpp/src/io/parquet/bloom_filter_reader.cu
user: login: lamarrr
body: why is this deleted?
created_at: 2025-05-15T02:36:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -584,8 +584,6 @@ std::optional<std::vector<std::vector<size_type>>> aggregate_reader_metadata::ap
                                             stream);
 }
 
-equality_literals_collector::equality_literals_collector() = default;
path: cpp/src/io/parquet/bloom_filter_reader.cu
user: login: mhaseeb123
body: Moved to the header where it's declared.
created_at: 2025-05-15T02:46:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: ttnghia
body: Please use initializer list instead of assignment.
created_at: 2025-05-15T03:21:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
+
+std::reference_wrapper<ast::expression const> dictionary_literals_collector::visit(
+  ast::operation const& expr)
+{
+  using cudf::ast::ast_operator;
+  auto const operands = expr.get_operands();
+  auto const op       = expr.get_operator();
+
+  if (auto* v = dynamic_cast<ast::column_reference const*>(&operands[0].get())) {
+    // First operand should be column reference, second should be literal.
+    CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2,
+                 "Only binary operations are supported on column reference");
+    auto const literal_ptr = dynamic_cast<ast::literal const*>(&operands[1].get());
+    CUDF_EXPECTS(literal_ptr != nullptr,
+                 "Second operand of binary operation with column reference must be a literal");
+    v->accept(*this);
+
+    // Push to the corresponding column's literals and operators list iff EQUAL or NOT_EQUAL
+    // operator is seen
+    if (op == ast_operator::EQUAL or op == ast::ast_operator::NOT_EQUAL) {
+      auto const col_idx = v->get_column_index();
+      _literals[col_idx].emplace_back(const_cast<ast::literal*>(literal_ptr));
+    }
+  } else {
+    // Visit the operands and ignore any output as we only want to collect literals
+    std::ignore = visit_operands(operands);
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: ttnghia
body: :+1: 
created_at: 2025-05-15T03:24:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,39 +243,238 @@ std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
   host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<int const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: ttnghia
body: I almost posted the same question :smile: 
created_at: 2025-05-15T03:26:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: Can't do that as these are member variables of the parent class. We also can't use a non-default constructor of the parent class here either as we don't want the parent class to `expr.accept(*this);`
created_at: 2025-05-15T03:26:24Z
author_association: MEMBER
diff_hunk: @@ -249,40 +264,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(thrust::make_tuple(
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: ttnghia
body: `make_zip_iterator` does not need to take in a tuple. Only constructing `thrust::zip_iterator` does.
created_at: 2025-05-15T03:28:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: ttnghia
body: But can we call the base class constructor, leaving `expr.accept` in this constructor body? I.e.,
```
dictionary_literals_collector(): base(...) {
  expr.accept(*this);
}
```
created_at: 2025-05-15T03:52:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hybrid_scan_helpers.hpp"
+
+#include <cudf/ast/detail/expression_transformer.hpp>
+#include <cudf/ast/detail/operators.hpp>
+#include <cudf/ast/expressions.hpp>
+#include <cudf/detail/cuco_helpers.hpp>
+#include <cudf/detail/transform.hpp>
+#include <cudf/hashing/detail/xxhash_64.cuh>
+#include <cudf/io/parquet_schema.hpp>
+#include <cudf/logger.hpp>
+#include <cudf/utilities/span.hpp>
+#include <cudf/utilities/traits.hpp>
+#include <cudf/utilities/type_checks.hpp>
+
+#include <rmm/cuda_stream_view.hpp>
+#include <rmm/device_buffer.hpp>
+#include <rmm/exec_policy.hpp>
+
+#include <thrust/iterator/counting_iterator.h>
+#include <thrust/tabulate.h>
+
+#include <future>
+#include <numeric>
+#include <optional>
+
+namespace cudf::io::parquet::experimental::detail {
+
+using parquet::detail::chunk_page_info;
+using parquet::detail::ColumnChunkDesc;
+using parquet::detail::decode_error;
+using parquet::detail::PageInfo;
+
+dictionary_literals_collector::dictionary_literals_collector(ast::expression const& expr,
+                                                             cudf::size_type num_input_columns)
+{
+  _num_input_columns = num_input_columns;
+  _literals.resize(num_input_columns);
+  expr.accept(*this);
+}
path: cpp/src/io/parquet/experimental/dictionary_page_filter.cu
user: login: mhaseeb123
body: There's a `expr.accept(*this);` in the constructor of the base class as well which we don't call being called.
created_at: 2025-05-15T03:58:39Z
author_association: MEMBER
diff_hunk: @@ -368,7 +373,11 @@ class hybrid_scan_reader {
   /**
    * @brief Filter the row groups using column chunk bloom filters
    *
-   * @param bloom_filter_data Device buffers containing per-column-chunk bloom filter data
+   * @note The `bloom_filter_data` device buffers must be allocated using a 32
+   *       byte aligned memory resource
+   *
+   * @param bloom_filter_data 32 byte aligned device buffers containing bloom filter data of column
+                              chunks with equality predicate.
path: cpp/include/cudf/io/experimental/hybrid_scan.hpp
user: login: ttnghia
body: Reformat?
created_at: 2025-05-15T17:39:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -249,40 +264,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(thrust::counting_iterator<cudf::size_type>(0),
+                                        output_column_schemas.begin());
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<thrust::tuple<cudf::size_type, cudf::size_type>> dictionary_col_schemas;
+  thrust::copy_if(thrust::host,
+                  iter,
+                  iter + output_column_schemas.size(),
+                  literals.begin(),
+                  std::back_inserter(dictionary_col_schemas),
+                  [](auto& dict_literals) { return not dict_literals.empty(); });
+
+  // No (in)equality literals found, return empty vector
+  if (dictionary_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_dictionary_columns = dictionary_col_schemas.size();
+  auto const num_chunks             = total_row_groups * num_dictionary_columns;
+
+  std::vector<byte_range_info> dictionary_page_bytes;
+  dictionary_page_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid dictionary page
+  auto have_dictionary_pages = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: ttnghia
body: Nit: This loop is called multiple times. Can we extract it to a common function?
created_at: 2025-05-15T17:41:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -249,40 +264,238 @@ aggregate_reader_metadata::select_payload_columns(
 std::vector<std::vector<cudf::size_type>> aggregate_reader_metadata::filter_row_groups_with_stats(
   host_span<std::vector<cudf::size_type> const> row_group_indices,
   host_span<data_type const> output_dtypes,
-  host_span<int const> output_column_schemas,
-  std::optional<std::reference_wrapper<ast::expression const>> filter,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter,
   rmm::cuda_stream_view stream) const
 {
-  // Return all row groups if no filter expression
-  if (not filter.has_value()) { return all_row_group_indices(row_group_indices); }
-
   // Compute total number of input row groups
-  cudf::size_type total_row_groups = [&]() {
-    if (not row_group_indices.empty()) {
-      size_t const total_row_groups =
-        std::accumulate(row_group_indices.begin(),
-                        row_group_indices.end(),
-                        size_t{0},
-                        [](auto sum, auto const& pfm) { return sum + pfm.size(); });
-
-      // Check if we have less than 2B total row groups.
-      CUDF_EXPECTS(total_row_groups <= std::numeric_limits<cudf::size_type>::max(),
-                   "Total number of row groups exceed the cudf::size_type's limit");
-      return static_cast<cudf::size_type>(total_row_groups);
-    } else {
-      return num_row_groups;
-    }
-  }();
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
 
   // Filter stats table with StatsAST expression and collect filtered row group indices
-  auto const stats_filtered_row_group_indices = apply_stats_filters(row_group_indices,
-                                                                    total_row_groups,
-                                                                    output_dtypes,
-                                                                    output_column_schemas,
-                                                                    filter.value(),
-                                                                    stream);
+  auto const stats_filtered_row_group_indices = apply_stats_filters(
+    row_group_indices, total_row_groups, output_dtypes, output_column_schemas, filter, stream);
 
   return stats_filtered_row_group_indices.value_or(all_row_group_indices(row_group_indices));
 }
 
+std::vector<byte_range_info> aggregate_reader_metadata::get_bloom_filter_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect equality literals for each input table column
+  auto const literals =
+    equality_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<cudf::size_type> bloom_filter_col_schemas;
+  thrust::copy_if(thrust::host,
+                  output_column_schemas.begin(),
+                  output_column_schemas.end(),
+                  literals.begin(),
+                  std::back_inserter(bloom_filter_col_schemas),
+                  [](auto& bloom_filter_literals) { return not bloom_filter_literals.empty(); });
+
+  // No equality literals found, return empty vector
+  if (bloom_filter_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_bloom_filter_columns = bloom_filter_col_schemas.size();
+  auto const num_chunks               = total_row_groups * num_bloom_filter_columns;
+
+  std::vector<byte_range_info> bloom_filter_bytes;
+  bloom_filter_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid bloom filter offset
+  auto have_bloom_filters = false;
+
+  // For all sources
+  std::for_each(thrust::counting_iterator<size_t>(0),
+                thrust::counting_iterator(row_group_indices.size()),
+                [&](auto const src_index) {
+                  // Get all row group indices in the data source
+                  auto const& rg_indices = row_group_indices[src_index];
+                  // For all row groups
+                  std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) {
+                    // For all column chunks
+                    std::for_each(
+                      bloom_filter_col_schemas.begin(),
+                      bloom_filter_col_schemas.end(),
+                      [&](auto const schema_idx) {
+                        auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx);
+                        // Get bloom filter offsets and sizes
+                        bloom_filter_bytes.emplace_back(col_meta.bloom_filter_offset.value_or(0),
+                                                        col_meta.bloom_filter_length.value_or(0));
+
+                        // Set `have_bloom_filters` if `bloom_filter_offset` is valid
+                        if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; }
+                      });
+                  });
+                });
+
+  if (not have_bloom_filters) { return {}; }
+
+  return bloom_filter_bytes;
+}
+
+std::vector<byte_range_info> aggregate_reader_metadata::get_dictionary_page_bytes(
+  cudf::host_span<std::vector<cudf::size_type> const> row_group_indices,
+  host_span<data_type const> output_dtypes,
+  host_span<cudf::size_type const> output_column_schemas,
+  std::reference_wrapper<ast::expression const> filter)
+{
+  // Collect (in)equality literals for each input table column
+  auto const literals =
+    dictionary_literals_collector{filter.get(), static_cast<cudf::size_type>(output_dtypes.size())}
+      .get_literals();
+
+  auto iter = thrust::make_zip_iterator(thrust::counting_iterator<cudf::size_type>(0),
+                                        output_column_schemas.begin());
+
+  // Collect schema indices of columns with equality predicate(s)
+  std::vector<thrust::tuple<cudf::size_type, cudf::size_type>> dictionary_col_schemas;
+  thrust::copy_if(thrust::host,
+                  iter,
+                  iter + output_column_schemas.size(),
+                  literals.begin(),
+                  std::back_inserter(dictionary_col_schemas),
+                  [](auto& dict_literals) { return not dict_literals.empty(); });
+
+  // No (in)equality literals found, return empty vector
+  if (dictionary_col_schemas.empty()) { return {}; }
+
+  // Compute total number of input row groups
+  auto const total_row_groups = compute_total_row_groups(row_group_indices);
+
+  // Descriptors for all the chunks that make up the selected columns
+  auto const num_dictionary_columns = dictionary_col_schemas.size();
+  auto const num_chunks             = total_row_groups * num_dictionary_columns;
+
+  std::vector<byte_range_info> dictionary_page_bytes;
+  dictionary_page_bytes.reserve(num_chunks);
+
+  // Flag to check if we have at least one valid dictionary page
+  auto have_dictionary_pages = false;
+
+  // For all sources
+  std::for_each(
+    thrust::counting_iterator<size_t>(0),
+    thrust::counting_iterator(row_group_indices.size()),
path: cpp/src/io/parquet/experimental/hybrid_scan_helpers.cpp
user: login: mhaseeb123
body: The innermost loop bounds and contents are different in both locations of this loop. I had them consolidated in an earlier version of this PR but that makes the code unreadable.
created_at: 2025-05-15T17:58:06Z
author_association: MEMBER

PR comments:
user: login: copy-pr-bot[bot]
created_at: 2025-04-22T01:29:08Z
body: Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message [here](https://docs.gha-runners.nvidia.com/cpr/auto-sync).

author_association: NONE
user: login: mhaseeb123
created_at: 2025-05-06T20:51:20Z
body: /ok to test
author_association: MEMBER
user: login: mhaseeb123
created_at: 2025-05-15T20:15:38Z
body: /merge
author_association: MEMBER