Repository: ClickHouse/ClickHouse
File: src/Planner/PlannerJoinTree.cpp (C++)

Code before:
                    /// Pre-build additional table filter to know what columns it needs
                    const auto & table_expression_alias = table_expression->getOriginalAlias();
                    auto additional_filters_info_temp = buildAdditionalFiltersIfNeeded(
                        storage, table_expression_alias, table_expression_query_info, prewhere_info, planner_context);
                    if (additional_filters_info_temp)
                    {
                        for (const auto * input : additional_filters_info_temp->actions.getInputs())
                            columns_needed_by_other_filters.insert(input->result_name);
                    }

                    /// Clone prewhere actions and add required columns to outputs
                    auto prewhere_actions_clone = prewhere_actions->clone();
                    const auto prewhere_column_name = prewhere_actions_clone.getOutputs().at(0)->result_name;

                    /// Add columns needed by other filters to prewhere outputs
                    std::unordered_set<const ActionsDAG::Node *> nodes_to_add;
                    for (const auto * input : prewhere_actions_clone.getInputs())
                    {
                        if (columns_needed_by_other_filters.contains(input->result_name))
                            nodes_to_add.insert(input);
                    }

                    /// Remove column from nodes_to_add if it's already in prewhere output
                    auto & prewhere_outputs = prewhere_actions_clone.getOutputs();
                    for (const auto & output : prewhere_outputs)
                    {
                        auto it = nodes_to_add.find(output);
                        if (it != nodes_to_add.end())
                            nodes_to_add.erase(it);
                    }

                    prewhere_outputs.insert(prewhere_outputs.end(), nodes_to_add.begin(), nodes_to_add.end());

                    /// Check if prewhere filter column should be removed
                    const bool keep_for_query = std::ranges::contains(columns_names, prewhere_column_name);
                    const bool keep_for_filters = columns_needed_by_other_filters.contains(prewhere_column_name);
                    const bool remove_prewhere_column = !keep_for_query && !keep_for_filters;

                    where_filters.emplace_back(
                        FilterDAGInfo{
                            std::move(prewhere_actions_clone),
                            prewhere_column_name,
                            remove_prewhere_column},
                        makeDescription("Prewhere"));
                }
                else if (prewhere_actions)
                {
                    prewhere_info = std::make_shared<PrewhereInfo>();
                    prewhere_info->prewhere_actions = prewhere_actions->clone();
                    prewhere_info->prewhere_column_name = prewhere_actions->getOutputs().at(0)->result_name;
                    /// Do not remove prewhere column if it is needed later

Review comment (bug):
`nodes_to_add` is an `unordered_set`, and then its iteration order is used to append to `prewhere_actions_clone.getOutputs()`. Output order in `ActionsDAG` is observable (affects header/serialization/hash/debug output) and this makes the plan non-deterministic across runs/platforms. Use a deterministic order (e.g., iterate `prewhere_actions_clone.getInputs()` and append missing nodes in that order, while tracking membership with a set).
```suggestion
                    /// Add columns needed by other filters to prewhere outputs in a deterministic order.
                    /// We iterate over inputs (which have a stable order) and append missing ones.
                    auto & prewhere_outputs = prewhere_actions_clone.getOutputs();
                    std::unordered_set<const ActionsDAG::Node *> outputs_set{prewhere_outputs.begin(), prewhere_outputs.end()};
                    for (const auto * input : prewhere_actions_clone.getInputs())
                    {
                        if (!columns_needed_by_other_filters.contains(input->result_name))
                            continue;

                        if (outputs_set.contains(input))
                            continue;

                        prewhere_outputs.push_back(input);
                        outputs_set.insert(input);
                    }

                    /// Check if prewhere filter column should be removed
                    const bool keep_for_query = std::ranges::contains(columns_names, prewhere_column_name);
                    const bool keep_for_filters = columns_needed_by_other_filters.contains(prewhere_column_name);
                    const bool remove_prewhere_column = !keep_for_query && !keep_for_filters;
                    const bool keep_for_query = std::ranges::contains(columns_names, prewhere_column_name);
                    const bool keep_for_filters = columns_needed_by_other_filters.contains(prewhere_column_name);
                    const bool remove_prewhere_column = !keep_for_query && !keep_for_filters;
```

Code after:

                    /// Clone prewhere actions and add required columns to outputs
                    auto prewhere_actions_clone = prewhere_actions->clone();
                    const auto prewhere_column_name = prewhere_actions_clone.getOutputs().at(0)->result_name;

                    /// Add columns needed by other filters to prewhere outputs
                    auto & prewhere_outputs = prewhere_actions_clone.getOutputs();

                    /// Build set of existing outputs for fast lookup
                    std::unordered_set<const ActionsDAG::Node *> existing_outputs(
                        prewhere_outputs.begin(), prewhere_outputs.end());

                    /// Iterate inputs in deterministic order and add missing nodes
                    for (const auto * input : prewhere_actions_clone.getInputs())
                    {
                        if (columns_needed_by_other_filters.contains(input->result_name)
                            && !existing_outputs.contains(input))
                        {
                            prewhere_outputs.push_back(input);
                        }
                    }

                    /// Check if prewhere filter column should be removed
                    const bool keep_for_query = std::ranges::contains(columns_names, prewhere_column_name);
                    const bool keep_for_filters = columns_needed_by_other_filters.contains(prewhere_column_name);
                    const bool remove_prewhere_column = !keep_for_query && !keep_for_filters;

                    where_filters.emplace_back(
                        FilterDAGInfo{
                            std::move(prewhere_actions_clone),
                            prewhere_column_name,
                            remove_prewhere_column},
                        makeDescription("Prewhere"));
                }
                else if (prewhere_actions)
                {
                    prewhere_info = std::make_shared<PrewhereInfo>();
                    prewhere_info->prewhere_actions = prewhere_actions->clone();
                    prewhere_info->prewhere_column_name = prewhere_actions->getOutputs().at(0)->result_name;
                    /// Do not remove prewhere column if it is needed later
                    bool keep_prewhere_column = std::ranges::contains(columns_names, prewhere_info->prewhere_column_name);
                    prewhere_info->remove_prewhere_column = !keep_prewhere_column;
                    prewhere_info->need_filter = true;
                }

                updatePrewhereOutputsIfNeeded(table_expression_query_info, table_expression_data.getColumnNames(), storage_snapshot);

                auto row_policy_filter_info
                    = buildRowPolicyFilterIfNeeded(storage, table_expression_query_info, planner_context, used_row_policies);
                if (row_policy_filter_info)
                {