Skip to content
Open
39 changes: 36 additions & 3 deletions src/TableFunctions/TableFunctionEval.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include <Core/Settings.h>
#include <DataTypes/DataTypeLowCardinality.h>
#include <DataTypes/IDataType.h>
#include <Interpreters/ApplyWithGlobalVisitor.h>
#include <Interpreters/Context.h>
#include <Interpreters/InterpreterSelectQueryAnalyzer.h>
#include <Interpreters/InterpreterSetQuery.h>
#include <Interpreters/NormalizeSelectWithUnionQueryVisitor.h>
#include <Interpreters/SelectIntersectExceptQueryVisitor.h>
#include <Interpreters/evaluateConstantExpression.h>
Expand All @@ -24,8 +26,11 @@ namespace Setting
{
extern const SettingsBool allow_experimental_analyzer;
extern const SettingsBool allow_experimental_eval_table_function;
extern const SettingsBool enable_global_with_statement;
extern const SettingsSetOperationMode except_default_mode;
extern const SettingsSetOperationMode intersect_default_mode;
extern const SettingsUInt64 max_ast_depth;
extern const SettingsUInt64 max_ast_elements;
extern const SettingsUInt64 max_parser_backtracks;
extern const SettingsUInt64 max_parser_depth;
extern const SettingsUInt64 max_query_size;
Expand Down Expand Up @@ -149,19 +154,47 @@ void TableFunctionEval::parseArguments(const ASTPtr & ast_function, ContextPtr c
/// `eval` is analyzer-only, and the same validation rejects such a change for a usual query.
validateAnalyzerSettings(query, settings[Setting::allow_experimental_analyzer]);

/// Resolve the generated query's own `SETTINGS` clause into a private context, before the
/// normalization visitors below rewrite the query tree (which can move or drop the `SETTINGS`).
/// The AST size limits are then read from this context, so an inner `... SETTINGS max_ast_elements = N`
/// controls its own limits the same way it would when the query is executed directly.
auto limits_context = Context::createCopy(context);
InterpreterSetQuery::applySettingsFromQuery(query, limits_context);
Comment thread
clickhouse-gh[bot] marked this conversation as resolved.
Comment thread
clickhouse-gh[bot] marked this conversation as resolved.
const auto & inner_settings = limits_context->getSettingsRef();

/// Expand global `WITH` aliases before the size limits below, same as `executeQueryImpl` does
/// (and gated by the inner query's own `enable_global_with_statement`). Otherwise the size limits
/// would run on the pre-expansion AST, so a generated query whose global CTE expands past
/// `max_ast_elements` / `max_ast_depth` would pass in `eval` while executing it directly is rejected.
if (inner_settings[Setting::enable_global_with_statement])
ApplyWithGlobalVisitor::visit(query);

/// The generated query does not go through `executeQuery`, so resolve the INTERSECT/EXCEPT
/// operator precedence and the implicit UNION mode here, same as `executeQueryImpl` does
/// for a usual query.
/// for a usual query. These modes are read from the inner query's own context, so that an
/// inner `... SETTINGS union_default_mode = 'DISTINCT'` normalizes the same way it would when
/// the query is executed directly, instead of being resolved against the outer defaults.
{
SelectIntersectExceptQueryVisitor::Data data{
settings[Setting::intersect_default_mode], settings[Setting::except_default_mode]};
inner_settings[Setting::intersect_default_mode], inner_settings[Setting::except_default_mode]};
SelectIntersectExceptQueryVisitor{data}.visit(query);
}
{
NormalizeSelectWithUnionQueryVisitor::Data data{settings[Setting::union_default_mode]};
NormalizeSelectWithUnionQueryVisitor::Data data{inner_settings[Setting::union_default_mode]};
NormalizeSelectWithUnionQueryVisitor{data}.visit(query);
}

/// Apply the AST size limits to the generated query, same as `executeQueryImpl` does for a usual
Comment thread
clickhouse-gh[bot] marked this conversation as resolved.
/// query. Without this, `max_ast_depth` / `max_ast_elements` are ineffective for the inner query:
/// a tiny outer `SELECT * FROM eval('...')` could smuggle a huge or very deep AST into the analyzer,
/// even though executing the same inner query directly would be rejected.
{
if (inner_settings[Setting::max_ast_depth])
query->checkDepth(inner_settings[Setting::max_ast_depth]);
if (inner_settings[Setting::max_ast_elements])
query->checkSize(inner_settings[Setting::max_ast_elements]);
}

create.set(create.select, query);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1
2
3
55
35 changes: 35 additions & 0 deletions tests/queries/0_stateless/04513_eval_table_function_ast_limits.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Tags: no-old-analyzer
-- no-old-analyzer: `eval` requires the analyzer.

-- The generated query is analyzed with the generated query's own SETTINGS, so its set operation modes
-- and AST size limits behave the same as when the query is executed directly.
-- See https://github.com/ClickHouse/ClickHouse/pull/110211

SET allow_experimental_eval_table_function = 1;

-- The set operation modes are resolved from the generated query's own SETTINGS, so an inner
-- `union_default_mode = 'DISTINCT'` normalizes an ambiguous UNION the same way it would when the
-- query is executed directly, even though the outer default would reject it.
SELECT count() FROM eval('SELECT 1 AS n UNION SELECT 1 AS n SETTINGS union_default_mode = ''DISTINCT''');
Comment thread
clickhouse-gh[bot] marked this conversation as resolved.
SELECT count() FROM eval('SELECT 1 AS n UNION SELECT 1 AS n'); -- { serverError EXPECTED_ALL_OR_DISTINCT }

-- INTERSECT / EXCEPT default modes are likewise resolved from the generated query's own SETTINGS.
-- These modes live in separate branches of `SelectIntersectExceptQueryVisitor`, so the inner
-- `intersect_default_mode` / `except_default_mode = 'ALL'` must win over the outer `'DISTINCT'`,
-- keeping duplicates exactly as when the query is executed directly. INTERSECT ALL of [1,1,2] and
-- [1,1,3] keeps two 1s; EXCEPT ALL of [1,1,1,2] and [1] keeps 1,1,2.
SELECT count() FROM eval('SELECT arrayJoin([1, 1, 2]) AS n INTERSECT SELECT arrayJoin([1, 1, 3]) AS n SETTINGS intersect_default_mode = ''ALL''') SETTINGS intersect_default_mode = 'DISTINCT';
SELECT count() FROM eval('SELECT arrayJoin([1, 1, 1, 2]) AS n EXCEPT SELECT arrayJoin([1]) AS n SETTINGS except_default_mode = ''ALL''') SETTINGS except_default_mode = 'DISTINCT';

-- The AST size limits `max_ast_elements` / `max_ast_depth` apply to the generated query, same as when
-- it is executed directly, so a tiny outer query cannot smuggle a huge or deep AST past them.
SELECT count() FROM eval('SELECT 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10') SETTINGS max_ast_elements = 30; -- { serverError TOO_BIG_AST }
SELECT count() FROM eval('SELECT (((((1)))))') SETTINGS max_ast_depth = 3; -- { serverError TOO_DEEP_AST }
-- The limits are read after the generated query's own SETTINGS are applied, so an inner SETTINGS clause
-- controls its own AST size limits, both to tighten and to relax them.
SELECT count() FROM eval('SELECT 1 + 2 + 3 + 4 + 5 SETTINGS max_ast_elements = 5'); -- { serverError TOO_BIG_AST }
SELECT * FROM eval('SELECT 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 AS big SETTINGS max_ast_elements = 100000') SETTINGS max_ast_elements = 30;
-- The size limits are checked after the global `WITH` aliases are expanded (as in a direct query), so a
-- global CTE that stays small before expansion but grows past `max_ast_elements` once it is inlined into
-- every UNION branch is rejected, instead of slipping through the pre-expansion check.
SELECT count() FROM eval('WITH 1+2+3+4+5+6+7+8+9+10+11+12+13+14+15 AS big SELECT big UNION ALL SELECT big UNION ALL SELECT big UNION ALL SELECT big UNION ALL SELECT big UNION ALL SELECT big UNION ALL SELECT big UNION ALL SELECT big SETTINGS max_ast_elements = 100'); -- { serverError TOO_BIG_AST }
Loading