Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f91aa6
Allow JOIN filter pushdown when side column names do not match the jo…
ianton-ru Aug 21, 2026
af87b90
Copy left-only WHERE into IStorageCluster JOIN wraps so icebergCluste…
ianton-ru Aug 21, 2026
4e1b754
Reuse existing left-only predicate helper for IStorageCluster JOIN wraps
ianton-ru Aug 21, 2026
a54a78a
Do not copy wrap predicates onto the null-producing side of an outer …
ianton-ru Aug 24, 2026
dc2f772
Do not copy nondeterministic wrap predicates that would run twice
ianton-ru Aug 24, 2026
78a27b3
Remove unnecessary `no-parallel-replicas` tag from JOIN filter pushdo…
ianton-ru Aug 24, 2026
28b3de8
Do not copy wrap predicates onto `ASOF` right or `PASTE` JOIN sides
ianton-ru Aug 24, 2026
0e14162
Do not copy stateful wrap predicates that would run twice
ianton-ru Aug 24, 2026
707dcdc
Share JOIN prefilter side rules between wrap copy and filter pushdown
ianton-ru Aug 24, 2026
2018169
Pin JOIN filter pushdown EXPLAIN test against parallel replicas and r…
ianton-ru Aug 24, 2026
a1d4502
Drop non-function wrap predicates that depend on the other JOIN side
ianton-ru Aug 24, 2026
cdc4b28
Keep equivalent-key JOIN filter pushdown separate from side prefilter
ianton-ru Aug 24, 2026
d9ebb0b
Rebuild `IStorageCluster` listing when a later `applyFilters` predica…
ianton-ru Aug 24, 2026
cf210cb
Do not copy server-constant wrap predicates such as `hostName`
ianton-ru Aug 24, 2026
18a667b
Do not copy node-local wrap predicates such as `dictGet`
ianton-ru Aug 25, 2026
523f4a0
Merge branch 'antalya-26.6' into fix/join-filter-pushdown-through-rename
ianton-ru Aug 25, 2026
f9203a7
Fixes after review
ianton-ru Sep 2, 2026
f459b98
List `IStorageCluster` files once from the first listing filter
ianton-ru Sep 2, 2026
a5e8776
Hide object-storage credentials in `ReadFromCluster` EXPLAIN output
ianton-ru Sep 3, 2026
cc31f79
Remap JOIN-output aliases before partial filter pushdown
ianton-ru Sep 3, 2026
6a44388
AND later cluster listing filters onto the wrap predicate
ianton-ru Sep 3, 2026
9925af7
Attach cluster wrap listing filters only to `ReadFromCluster`
ianton-ru Sep 3, 2026
59af131
Merge branch 'antalya-26.6' into fix/join-filter-pushdown-through-rename
ianton-ru Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 84 additions & 10 deletions src/Analyzer/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

#include <Core/Streaming/CursorTree_fwd.h>

#include <functional>
#include <ranges>

namespace DB
Expand Down Expand Up @@ -1168,24 +1169,77 @@ bool hasUnknownColumn(const QueryTreeNodePtr & node, QueryTreeNodePtr table_expr
return false;
}

void removeExpressionsThatDoNotDependOnTableIdentifiers(
namespace
{

template <typename KeepFunction>
bool walkOrdinaryFunctions(const QueryTreeNodePtr & node, KeepFunction && keep_function)
{
QueryTreeNodes stack = {node};
while (!stack.empty())
{
auto current = std::move(stack.back());
stack.pop_back();
if (!current)
continue;

const auto type = current->getNodeType();
if (type == QueryTreeNodeType::QUERY || type == QueryTreeNodeType::UNION)
return false;

if (const auto * function = current->as<FunctionNode>())
{
if (function->isWindowFunction() || function->isAggregateFunction())
return false;
if (function->isOrdinaryFunction())

@k-morozov k-morozov Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we should to do if function not isOrdinaryFunction ? Just ignoring?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Window and aggregate functions are checked above, so here can be only ordinary or unresolved functions.
But all functions must be resolved when this method is called.
I rewrite test to be more clean, without logic change.

{
auto function_base = function->getFunction();
if (!function_base || !keep_function(function_base))
return false;
}
}

for (const auto & child : current->getChildren())
{
if (child)
stack.push_back(child);
}
}
return true;
}

bool isSafeToDuplicateInQueryTree(const QueryTreeNodePtr & node)
{
return walkOrdinaryFunctions(
node,
[](const FunctionBasePtr & function_base)
{
return function_base->isDeterministicInScopeOfQuery() && !function_base->isStateful();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject server constants when duplicating predicates

When an IStorageCluster join predicate contains a server-local constant such as hostName, this check treats it as safe because the function is deterministic within a query and non-stateful. The original predicate is evaluated on the initiator after the join, but its copied wrapper predicate is serialized and evaluated on each remote server, where hostName can return a different value; for example, WHERE hostName() = '<initiator>' can pass locally while the wrapper removes every remote row. Also reject functions whose isServerConstant flag is set before copying the conjunct.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

});
}

void filterConjunctions(
QueryTreeNodePtr & expression,
const QueryTreeNodePtr & table_expression,
const std::function<bool(const QueryTreeNodePtr &)> & keep,
const ContextPtr & context)
{
auto * function = expression->as<FunctionNode>();
if (!function)
{
if (!keep(expression))
expression = {};
return;
}

if (function->getFunctionName() != "and")
{
if (hasUnknownColumn(expression, table_expression))
expression = nullptr;
if (!keep(expression))
expression = {};
return;
}

QueryTreeNodesDeque conjunctions;
QueryTreeNodesDeque processing{ expression };
QueryTreeNodesDeque processing{expression};

while (!processing.empty())
{
Expand All @@ -1195,10 +1249,7 @@ void removeExpressionsThatDoNotDependOnTableIdentifiers(
if (auto * function_node = node->as<FunctionNode>())
{
if (function_node->getFunctionName() == "and")
std::ranges::copy(
function_node->getArguments(),
std::back_inserter(processing)
);
std::ranges::copy(function_node->getArguments(), std::back_inserter(processing));
else
conjunctions.push_back(node);
}
Expand All @@ -1212,7 +1263,7 @@ void removeExpressionsThatDoNotDependOnTableIdentifiers(

for (const auto & node : processing)
{
if (!hasUnknownColumn(node, table_expression))
if (keep(node))
conjunctions.push_back(node);
}

Expand All @@ -1234,6 +1285,29 @@ void removeExpressionsThatDoNotDependOnTableIdentifiers(
function->resolveAsFunction(function_impl->build(function->getArgumentColumns()));
}

}

void removeExpressionsThatDoNotDependOnTableIdentifiers(
QueryTreeNodePtr & expression,
const QueryTreeNodePtr & table_expression,
const ContextPtr & context)
{
filterConjunctions(
expression,
[&](const QueryTreeNodePtr & node) { return !hasUnknownColumn(node, table_expression); },
context);
}

void removeExpressionsThatAreUnsafeToDuplicate(
QueryTreeNodePtr & expression,
const ContextPtr & context)
{
if (!expression)
return;

filterConjunctions(expression, isSafeToDuplicateInQueryTree, context);
}

namespace
{

Expand Down
11 changes: 10 additions & 1 deletion src/Analyzer/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,22 @@ bool hasUnknownColumn(
/** Suppose we have a table x with columns a, c, d and
* a an expression like x.a > 2 AND y.b > 3 AND x.c + 1 == x.d
* This method will remove the part y.b > 3 from it since it depends
* on unknown columns from a different table.
* on unknown columns from a different table. A non-function root such as
* `WHERE y.b` is dropped the same way.
*/
void removeExpressionsThatDoNotDependOnTableIdentifiers(
QueryTreeNodePtr & expression,
const QueryTreeNodePtr & replacement_table_expression,
const ContextPtr & context);

/** Remove conjuncts that are unsafe to copy into another query tree (non-deterministic in this
* query, or stateful). Nested `and` is flattened the same way as
* `removeExpressionsThatDoNotDependOnTableIdentifiers`. Window and aggregate functions are also
* dropped. JOIN filter pushdown refuses stateful predicates via `ActionsDAG::hasStatefulFunctions`.
*/
void removeExpressionsThatAreUnsafeToDuplicate(
QueryTreeNodePtr & expression,
const ContextPtr & context);

Field getFieldFromColumnForASTLiteral(const ColumnPtr & column, size_t row, const DataTypePtr & data_type);

Expand Down
18 changes: 18 additions & 0 deletions src/Core/Joins.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ enum class JoinTableSide : uint8_t

const char * toString(JoinTableSide join_table_side);

/** Whether a post-JOIN `WHERE` conjunct on this side can be applied before the JOIN.
* Same rules as JOIN filter pushdown: skip the null-producing side of an outer JOIN,
* the right side of an `ASOF JOIN`, and both sides of a `PASTE JOIN` or `FULL JOIN`.
* Dictionary / lookup fill is a separate check (`JoinStep::allowPushDownToRight`).
*/
constexpr bool canPrefilterJoinSide(JoinKind kind, JoinStrictness strictness, JoinTableSide side)
{
if (isPaste(kind) || isFull(kind))
return false;
if (strictness == JoinStrictness::Asof && side == JoinTableSide::Right)
return false;
if (isLeft(kind) && side == JoinTableSide::Right)
return false;
if (isRight(kind) && side == JoinTableSide::Left)
return false;
return true;
}

enum class JoinOrderAlgorithm : uint8_t
{
GREEDY = 0,
Expand Down
10 changes: 10 additions & 0 deletions src/Planner/Planner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ void checkStoragesSupportTransactions(const PlannerContextPtr & planner_context)
}
}

}

namespace
{

/** Storages can rely that filters that for storage will be available for analysis before
* getQueryProcessingStage method will be called.
*
Expand Down Expand Up @@ -390,6 +395,8 @@ FiltersForTableExpressionMap collectFiltersForAnalysis(const QueryTreeNodePtr &
return res;
}

}

FiltersForTableExpressionMap collectFiltersForAnalysis(const QueryTreeNodePtr & query_tree_node, const SelectQueryOptions & select_query_options, const ActionsDAG * post_filter)
{
if (select_query_options.only_analyze)
Expand All @@ -411,6 +418,9 @@ FiltersForTableExpressionMap collectFiltersForAnalysis(const QueryTreeNodePtr &
return collectFiltersForAnalysis(query_tree_node, table_expressions_nodes, context, post_filter);
}

namespace
{

/// Extend lifetime of query context, storages, and table locks
void extendQueryContextAndStoragesLifetime(QueryPlan & query_plan, const PlannerContextPtr & planner_context)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Planner/Planner.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <Processors/QueryPlan/QueryPlan.h>
#include <Storages/SelectQueryInfo.h>
#include <Planner/PlannerContext.h>

namespace DB
{
Expand Down Expand Up @@ -89,4 +90,9 @@ class Planner
QueryNodeToPlanStepMapping query_node_to_plan_step_mapping;
};

FiltersForTableExpressionMap collectFiltersForAnalysis(
const QueryTreeNodePtr & query_tree_node,
const SelectQueryOptions & select_query_options,
const ActionsDAG * post_filter);

}
Loading
Loading