github-actions[bot] commented on code in PR #65369:
URL: https://github.com/apache/doris/pull/65369#discussion_r3549337294


##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -597,42 +780,490 @@ Status 
ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
     return Status::OK();
 }
 
+namespace {
+
+struct PredicateConjunctSchedule {
+    std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
+    VExprContextSPtrs remaining_conjuncts;
+};
+
+PredicateConjunctSchedule build_predicate_conjunct_schedule(
+        const format::FileScanRequest& request) {
+    std::unordered_set<size_t> predicate_block_positions;
+    predicate_block_positions.reserve(request.predicate_columns.size());
+    for (const auto& col : request.predicate_columns) {
+        const auto position_it = request.local_positions.find(col.column_id());
+        DORIS_CHECK(position_it != request.local_positions.end());
+        predicate_block_positions.insert(position_it->second.value());
+    }
+
+    PredicateConjunctSchedule schedule;
+    for (const auto& conjunct : request.conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        std::set<int> referenced_positions;
+        conjunct->root()->collect_slot_column_ids(referenced_positions);
+        if (referenced_positions.size() != 1) {
+            schedule.remaining_conjuncts.push_back(conjunct);
+            continue;
+        }
+        const auto block_position = 
static_cast<size_t>(*referenced_positions.begin());
+        if (!predicate_block_positions.contains(block_position)) {
+            schedule.remaining_conjuncts.push_back(conjunct);
+            continue;
+        }
+        schedule.single_column_conjuncts[block_position].push_back(conjunct);
+    }
+    return schedule;
+}
+
+bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
+    if (conjuncts.empty()) {
+        return false;
+    }
+    return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
+        return conjunct != nullptr && conjunct->root() != nullptr &&
+               conjunct->root()->can_evaluate_dictionary_filter();
+    });
+}
+
+void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context, 
const VExprSPtr& expr,
+                                       DictionaryResidualConjuncts* 
residual_conjuncts) {
+    DORIS_CHECK(owner_context != nullptr);
+    DORIS_CHECK(expr != nullptr);
+    DORIS_CHECK(residual_conjuncts != nullptr);
+
+    // The dictionary interface is exact for a single dictionary-aware 
predicate and for OR only
+    // when every child is dictionary-aware. AND is different: VCompoundPred 
evaluates only the
+    // dictionary-aware children as a prefilter. Split AND recursively so the 
already-applied
+    // dictionary children are not executed again on materialized rows, while 
non-dictionary
+    // residual children still keep the original row-level semantics.
+    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
+    if (compound_pred != nullptr && compound_pred->op() == 
TExprOpcode::COMPOUND_AND) {
+        for (const auto& child : expr->children()) {
+            collect_dictionary_residual_exprs(owner_context, child, 
residual_conjuncts);
+        }
+        return;
+    }
+
+    if (!expr->can_evaluate_dictionary_filter()) {
+        residual_conjuncts->emplace_back(owner_context, expr);
+    }
+}
+
+DictionaryResidualConjuncts build_dictionary_residual_conjuncts(
+        const VExprContextSPtrs& conjuncts) {
+    DictionaryResidualConjuncts residual_conjuncts;
+    for (const auto& conjunct : conjuncts) {
+        DORIS_CHECK(conjunct != nullptr);
+        collect_dictionary_residual_exprs(conjunct, conjunct->root(), 
&residual_conjuncts);
+    }
+    return residual_conjuncts;
+}
+
+uint16_t count_selected_rows(const IColumn::Filter& filter) {
+    uint16_t selected_rows = 0;
+    for (const auto value : filter) {
+        selected_rows += value != 0;
+    }
+    return selected_rows;
+}
+
+Status filter_read_predicate_columns(Block* file_block, const 
std::vector<uint32_t>& positions,
+                                     const IColumn::Filter& compact_filter) {
+    if (positions.empty()) {
+        return Status::OK();
+    }
+    RETURN_IF_CATCH_EXCEPTION(Block::filter_block_internal(file_block, 
positions, compact_filter));
+    return Status::OK();
+}
+
+IColumn::Filter build_dictionary_entry_filter(size_t block_position,
+                                              const ParquetColumnSchema& 
column_schema,
+                                              const VExprContextSPtrs& 
conjuncts,
+                                              const ParquetDictionaryWords& 
dict_words) {
+    auto fields = dictionary_fields_from_words(dict_words);
+    IColumn::Filter dictionary_filter(fields.size(), 1);
+    DictionaryEvalContext ctx;
+    auto& slot = ctx.slots
+                         .emplace(static_cast<int>(block_position),
+                                  DictionaryEvalContext::SlotDictionary {
+                                          .data_type = column_schema.type, 
.values = {}})
+                         .first->second;
+    slot.values.reserve(1);
+
+    for (size_t dict_idx = 0; dict_idx < fields.size(); ++dict_idx) {
+        slot.values.clear();
+        slot.values.push_back(fields[dict_idx]);
+        dictionary_filter[dict_idx] = 
VExprContext::evaluate_dictionary_filter(conjuncts, ctx) ==
+                                                      
ZoneMapFilterResult::kNoMatch
+                                              ? 0
+                                              : 1;
+    }
+    return dictionary_filter;
+}
+
+} // namespace
+
+Status ParquetScanScheduler::prepare_current_dictionary_filters(
+        ParquetFileContext& file_context,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const format::FileScanRequest& request, int row_group_idx,
+        const ::parquet::RowGroupMetaData& row_group_metadata) {
+    _current_dictionary_filters.clear();
+    _current_dictionary_residual_conjuncts.clear();
+    if (request.conjuncts.empty()) {
+        return Status::OK();
+    }
+    PredicateConjunctSchedule schedule;
+    {
+        SCOPED_TIMER(_scan_profile.dict_filter_expr_rewrite_time);
+        schedule = build_predicate_conjunct_schedule(request);
+    }
+    if (schedule.single_column_conjuncts.empty()) {
+        return Status::OK();
+    }
+
+    SCOPED_TIMER(_scan_profile.dict_filter_rewrite_time);
+    for (const auto& col : request.predicate_columns) {
+        const auto local_id = col.local_id();
+        if (local_id < 0 || local_id >= 
static_cast<int32_t>(file_schema.size())) {
+            continue;
+        }
+        const auto position_it = request.local_positions.find(col.column_id());
+        DORIS_CHECK(position_it != request.local_positions.end());
+        const auto block_position = 
static_cast<size_t>(position_it->second.value());
+        const auto conjunct_it = 
schedule.single_column_conjuncts.find(block_position);
+        if (conjunct_it == schedule.single_column_conjuncts.end() ||
+            !can_evaluate_all_with_dictionary(conjunct_it->second)) {
+            continue;
+        }
+        
update_counter_if_not_null(_scan_profile.dict_filter_candidate_columns, 1);
+
+        // This optimization is deliberately limited to single-column 
predicates with a dictionary
+        // evaluable part. Mixed AND predicates are split so 
dictionary-covered children run as a
+        // dict-id prefilter and residual children keep the normal row-level 
expression path.
+        const auto& column_schema = file_schema[local_id];
+        DORIS_CHECK(column_schema != nullptr);
+        if (column_schema->leaf_column_id < 0 ||
+            column_schema->leaf_column_id >= row_group_metadata.num_columns()) 
{
+            
update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
+            continue;
+        }
+        auto column_chunk = 
row_group_metadata.ColumnChunk(column_schema->leaf_column_id);
+        if (column_chunk == nullptr ||
+            !supports_row_level_dictionary_filter(*column_schema, 
*column_chunk)) {
+            
update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
+            continue;
+        }
+
+        ParquetDictionaryWords dict_words;
+        {
+            SCOPED_TIMER(_scan_profile.dict_filter_read_dict_time);
+            if (!read_dictionary_words(file_context.file_reader.get(), 
row_group_idx,
+                                       column_schema->leaf_column_id, 
*column_schema,
+                                       &dict_words)) {
+                
update_counter_if_not_null(_scan_profile.dict_filter_read_failures, 1);
+                continue;
+            }
+        }
+
+        // Build a safe dictionary prefilter from the dictionary-filter 
interface instead of
+        // executing the row expression on a temporary dictionary block. For 
compound AND,
+        // VCompoundPred intentionally evaluates only dictionary-capable 
children, so residual
+        // predicates still run later on surviving rows.
+        IColumn::Filter dictionary_filter;
+        DictionaryResidualConjuncts residual_conjuncts;
+        {
+            SCOPED_TIMER(_scan_profile.dict_filter_build_time);
+            dictionary_filter = build_dictionary_entry_filter(block_position, 
*column_schema,
+                                                              
conjunct_it->second, dict_words);
+            residual_conjuncts = 
build_dictionary_residual_conjuncts(conjunct_it->second);
+        }
+
+        // The bitmap is keyed by Parquet dictionary id. Later data-page reads 
evaluate the
+        // predicate with an integer lookup and only materialize STRING values 
for surviving rows.
+        _current_dictionary_filters.emplace(local_id, 
std::move(dictionary_filter));
+        _current_dictionary_residual_conjuncts.emplace(local_id, 
std::move(residual_conjuncts));
+        update_counter_if_not_null(_scan_profile.dict_filter_columns, 1);
+    }
+    return Status::OK();
+}
+
 Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
                                                  const 
format::FileScanRequest& request,
                                                  Block* file_block, 
SelectionVector* selection,
                                                  uint16_t* selected_rows,
-                                                 int64_t* 
conjunct_filtered_rows) {
+                                                 int64_t* 
conjunct_filtered_rows,
+                                                 bool* 
predicate_columns_filtered) {
+    DORIS_CHECK(predicate_columns_filtered != nullptr);
+    *predicate_columns_filtered = false;
     if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
         selection->resize(static_cast<size_t>(batch_rows));
     }
-    for (const auto& [fid, column_reader] : _current_predicate_columns) {
-        auto position_it = 
request.local_positions.find(format::LocalColumnId(fid));
-        DORIS_CHECK(position_it != request.local_positions.end());
-        const auto block_position = position_it->second.value();
+    const auto schedule = build_predicate_conjunct_schedule(request);
+    const bool can_read_predicate_columns_round_by_round =
+            !schedule.single_column_conjuncts.empty();
+    std::vector<uint32_t> read_column_positions;
+    read_column_positions.reserve(request.predicate_columns.size());
+
+    auto read_predicate_column = [&](ParquetColumnReader* column_reader, 
size_t block_position,
+                                     ColumnId local_id, bool* 
used_dictionary_filter) -> Status {
+        DORIS_CHECK(used_dictionary_filter != nullptr);
+        *used_dictionary_filter = false;
         DCHECK(remove_nullable(column_reader->type())
                        
->equals(*remove_nullable(file_block->get_by_position(block_position).type)))
                 << column_reader->type()->get_name() << " "
                 << 
file_block->get_by_position(block_position).type->get_name() << " "
                 << column_reader->name() << " " << 
file_block->get_by_position(block_position).name;
         auto column = 
file_block->get_by_position(block_position).column->assert_mutable();
-        int64_t column_rows = 0;
-        {
-            SCOPED_TIMER(_scan_profile.column_read_time);
-            RETURN_IF_ERROR(column_reader->read(batch_rows, column, 
&column_rows));
+        SCOPED_TIMER(_scan_profile.column_read_time);
+        const auto dictionary_filter_it = 
_current_dictionary_filters.find(local_id);
+        if (dictionary_filter_it != _current_dictionary_filters.end()) {
+            const uint16_t selected_rows_before = *selected_rows;
+            IColumn::Filter compact_filter;
+            bool used_filter = false;
+            RETURN_IF_ERROR(column_reader->select_with_dictionary_filter(
+                    *selection, *selected_rows, batch_rows, 
dictionary_filter_it->second, column,
+                    &compact_filter, &used_filter));
+            if (used_filter) {
+                DORIS_CHECK(compact_filter.size() == selected_rows_before);
+                const uint16_t new_selected_rows = 
count_selected_rows(compact_filter);
+                const auto filtered_rows = 
static_cast<int64_t>(selected_rows_before) -
+                                           
static_cast<int64_t>(new_selected_rows);
+                if (conjunct_filtered_rows != nullptr) {
+                    *conjunct_filtered_rows += filtered_rows;
+                }
+                
update_counter_if_not_null(_scan_profile.rows_filtered_by_dict_filter,
+                                           filtered_rows);
+                if (new_selected_rows != selected_rows_before) {
+                    // The dictionary reader has already appended only 
surviving values for the
+                    // current column. Apply the compact row filter only to 
columns read before this
+                    // one, then update the shared selection for later 
predicate/lazy columns.
+                    RETURN_IF_ERROR(filter_read_predicate_columns(file_block, 
read_column_positions,
+                                                                  
compact_filter));
+                    *selected_rows = 
apply_compact_filter_to_selection(compact_filter, selection,
+                                                                       
selected_rows_before);
+                    *predicate_columns_filtered = true;
+                }
+                file_block->replace_by_position(block_position, 
std::move(column));
+                
read_column_positions.push_back(cast_set<uint32_t>(block_position));
+                *used_dictionary_filter = true;
+                return Status::OK();
+            }
         }
-        if (column_rows != batch_rows) {
-            return Status::Corruption("Parquet filter column {} returned {} 
rows, expected {} rows",
-                                      column_reader->name(), column_rows, 
batch_rows);
+
+        if (*selected_rows == batch_rows) {
+            int64_t column_rows = 0;
+            RETURN_IF_ERROR(column_reader->read(batch_rows, column, 
&column_rows));
+            if (column_rows != batch_rows) {
+                return Status::Corruption(
+                        "Parquet filter column {} returned {} rows, expected 
{} rows",
+                        column_reader->name(), column_rows, batch_rows);
+            }
+        } else {
+            [[maybe_unused]] auto old_size = column->size();
+            RETURN_IF_ERROR(column_reader->select(*selection, *selected_rows, 
batch_rows, column));
+            if (column->size() != old_size + *selected_rows) {
+                return Status::Corruption(
+                        "Parquet selected filter column {} returned {} rows, 
expected {} rows",
+                        column_reader->name(), column->size(), old_size + 
*selected_rows);
+            }
+            *predicate_columns_filtered = true;
         }
         file_block->replace_by_position(block_position, std::move(column));
-    }
-    if (_scan_profile.predicate_filter_time == nullptr) {
+        read_column_positions.push_back(cast_set<uint32_t>(block_position));
+        return Status::OK();
+    };
+
+    auto execute_scheduled_conjuncts = [&](const VExprContextSPtrs& conjuncts) 
-> Status {
+        if (conjuncts.empty() || *selected_rows == 0) {
+            return Status::OK();
+        }
+        const uint16_t selected_rows_before = *selected_rows;
+        IColumn::Filter compact_filter;
+        bool can_filter_all = false;
+        RETURN_IF_ERROR(execute_compact_filter_conjuncts(
+                conjuncts, selected_rows_before, file_block, &compact_filter, 
&can_filter_all));
+        if (can_filter_all) {
+            compact_filter.resize_fill(selected_rows_before, 0);
+        }
+        const uint16_t new_selected_rows = can_filter_all ? 0 : 
count_selected_rows(compact_filter);
+        if (conjunct_filtered_rows != nullptr) {
+            *conjunct_filtered_rows += 
static_cast<int64_t>(selected_rows_before) -
+                                       static_cast<int64_t>(new_selected_rows);
+        }
+        if (new_selected_rows != selected_rows_before) {
+            // All columns read so far are already compacted to the current 
selection. Apply the
+            // compact filter to those columns and the selection vector 
together, so later predicate
+            // columns can read only rows that survived previous predicate 
rounds.
+            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, 
read_column_positions,
+                                                          compact_filter));
+            *selected_rows = can_filter_all
+                                     ? 0
+                                     : 
apply_compact_filter_to_selection(compact_filter, selection,
+                                                                         
selected_rows_before);
+            *predicate_columns_filtered = true;
+        }
+        return Status::OK();
+    };
+
+    auto execute_scheduled_dictionary_residual_conjuncts =
+            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+        if (conjuncts.empty() || *selected_rows == 0) {
+            return Status::OK();
+        }
+        const uint16_t selected_rows_before = *selected_rows;
+        IColumn::Filter compact_filter;
+        bool can_filter_all = false;
+        RETURN_IF_ERROR(execute_compact_dictionary_residual_conjuncts(
+                conjuncts, selected_rows_before, file_block, &compact_filter, 
&can_filter_all));
+        if (can_filter_all) {
+            compact_filter.resize_fill(selected_rows_before, 0);
+        }
+        const uint16_t new_selected_rows = can_filter_all ? 0 : 
count_selected_rows(compact_filter);
+        if (conjunct_filtered_rows != nullptr) {
+            *conjunct_filtered_rows += 
static_cast<int64_t>(selected_rows_before) -
+                                       static_cast<int64_t>(new_selected_rows);
+        }
+        if (new_selected_rows != selected_rows_before) {
+            // Dictionary-covered children have already reduced the compact 
block. Apply only the
+            // residual child filters here, then keep the same 
compacted-column invariant as the
+            // normal conjunct path for later predicate rounds.
+            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, 
read_column_positions,
+                                                          compact_filter));
+            *selected_rows = can_filter_all
+                                     ? 0
+                                     : 
apply_compact_filter_to_selection(compact_filter, selection,
+                                                                         
selected_rows_before);
+            *predicate_columns_filtered = true;
+        }
+        return Status::OK();
+    };
+
+    auto execute_scheduled_conjuncts_with_profile =
+            [&](const VExprContextSPtrs& conjuncts) -> Status {
+        if (_scan_profile.predicate_filter_time == nullptr) {
+            return execute_scheduled_conjuncts(conjuncts);
+        }
+        SCOPED_TIMER(_scan_profile.predicate_filter_time);
+        return execute_scheduled_conjuncts(conjuncts);
+    };
+
+    auto execute_scheduled_dictionary_residual_conjuncts_with_profile =
+            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
+        if (_scan_profile.predicate_filter_time == nullptr) {
+            return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+        }
+        SCOPED_TIMER(_scan_profile.predicate_filter_time);
+        return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
+    };
+
+    auto execute_scheduled_delete_conjuncts = [&]() -> Status {
+        if (request.delete_conjuncts.empty() || *selected_rows == 0) {
+            return Status::OK();
+        }
+        const uint16_t selected_rows_before = *selected_rows;
+        IColumn::Filter compact_filter;
+        bool can_filter_all = false;
+        
RETURN_IF_ERROR(execute_compact_delete_conjuncts(request.delete_conjuncts,
+                                                         selected_rows_before, 
file_block,
+                                                         &compact_filter, 
&can_filter_all));
+        if (can_filter_all) {
+            compact_filter.resize_fill(selected_rows_before, 0);
+        }
+        if (can_filter_all || count_selected_rows(compact_filter) != 
selected_rows_before) {
+            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, 
read_column_positions,
+                                                          compact_filter));
+            *selected_rows = can_filter_all
+                                     ? 0
+                                     : 
apply_compact_filter_to_selection(compact_filter, selection,
+                                                                         
selected_rows_before);
+            *predicate_columns_filtered = true;
+        }
+        return Status::OK();
+    };
+
+    auto read_all_predicate_columns = [&]() -> Status {
+        for (const auto& [fid, column_reader] : _current_predicate_columns) {
+            auto position_it = 
request.local_positions.find(format::LocalColumnId(fid));
+            DORIS_CHECK(position_it != request.local_positions.end());
+            bool used_dictionary_filter = false;
+            RETURN_IF_ERROR(read_predicate_column(column_reader.get(), 
position_it->second.value(),
+                                                  fid, 
&used_dictionary_filter));
+        }
+        return Status::OK();
+    };
+
+    if (!can_read_predicate_columns_round_by_round) {
+        RETURN_IF_ERROR(read_all_predicate_columns());
+        if (_scan_profile.predicate_filter_time == nullptr) {
+            return execute_batch_filters(request, batch_rows, file_block, 
selection, selected_rows,
+                                         conjunct_filtered_rows);
+        }
+        SCOPED_TIMER(_scan_profile.predicate_filter_time);
         return execute_batch_filters(request, batch_rows, file_block, 
selection, selected_rows,
                                      conjunct_filtered_rows);
     }
+
+    auto read_round_by_round = [&]() -> Status {
+        // Single-column conjuncts can be evaluated immediately after their 
column is read. Once
+        // selection shrinks, later predicate columns use 
ParquetColumnReader::select() so the
+        // reader skips rows already rejected by earlier predicates instead of 
materializing them.
+        for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
+            const auto& col = request.predicate_columns[idx];
+            const auto fid = col.local_id();
+            auto reader_it = _current_predicate_columns.find(fid);
+            DORIS_CHECK(reader_it != _current_predicate_columns.end());
+            auto position_it = request.local_positions.find(col.column_id());
+            DORIS_CHECK(position_it != request.local_positions.end());
+            const auto block_position = position_it->second.value();
+            bool used_dictionary_filter = false;
+            RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(), 
block_position, fid,
+                                                  &used_dictionary_filter));
+            if (*selected_rows == 0) {
+                for (size_t remaining_idx = idx + 1;
+                     remaining_idx < request.predicate_columns.size(); 
++remaining_idx) {
+                    const auto remaining_fid = 
request.predicate_columns[remaining_idx].local_id();
+                    auto remaining_reader_it = 
_current_predicate_columns.find(remaining_fid);
+                    DORIS_CHECK(remaining_reader_it != 
_current_predicate_columns.end());
+                    
RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
+                }
+                return Status::OK();
+            }
+            const auto conjunct_it = 
schedule.single_column_conjuncts.find(block_position);
+            if (conjunct_it == schedule.single_column_conjuncts.end()) {
+                continue;
+            }
+            if (used_dictionary_filter) {
+                const auto residual_it = 
_current_dictionary_residual_conjuncts.find(fid);
+                DORIS_CHECK(residual_it != 
_current_dictionary_residual_conjuncts.end());
+                
RETURN_IF_ERROR(execute_scheduled_dictionary_residual_conjuncts_with_profile(
+                        residual_it->second));
+            } else {
+                
RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));

Review Comment:
   This round-by-round path should not schedule volatile single-column 
conjuncts. `localize_filters()` pushes any localizable table filter into 
`file_request->conjuncts`, and `build_predicate_conjunct_schedule()` groups 
them only by referenced slot count. After an earlier predicate shrinks 
`selected_rows`, the later predicate column is read with `select()` and this 
call evaluates the conjunct on the compacted survivor block only.
   
   That changes the stream consumed by stateful functions such as `rand(1)`: 
before this PR, `execute_filter_conjuncts()` executed every conjunct with 
`rows=batch_rows` on the full predicate block and then applied the result to 
the current `SelectionVector`, so a predicate like `a > 0 AND b + rand(1) > 5` 
paired survivors with the random values for their original batch positions. Now 
the `b + rand(1)` conjunct consumes only one random value per row that survived 
`a > 0`, changing which rows pass. Please keep volatile/non-deterministic 
expressions on the old full-batch path, or only split deterministic 
single-column conjuncts into the round-by-round schedule.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to