Type IN (subquery) over LowCardinality consistently in the old analyzer - #114856
Conversation
…d analyzer
The old analyzer replaces `in` with `inIgnoreSet` while it only needs the
types, passing the left operand as the stand-in for the set that is not built
yet. The general typing rule wraps the result in `LowCardinality` only when
every other argument is a constant, so two full `LowCardinality` arguments
typed the expression as plain `UInt8`, while executing the real `in` against
the constant `Set` column yields `LowCardinality(UInt8)`.
A query reading such a column across a subquery boundary then recorded
`UInt8` in the outer `ActionsDAG` and got `LowCardinality(UInt8)` in the
header, failing the type check in `ActionsDAG::updateHeader`:
SET enable_analyzer = 0;
SELECT tuple(*) FROM (SELECT s, s IN (SELECT 'a') AS f FROM t);
-- Unexpected return type from tuple. Expected Tuple(LowCardinality(String), UInt8).
-- Got Tuple(LowCardinality(String), LowCardinality(UInt8)). (LOGICAL_ERROR)
Pass a constant `UInt8` as the stand-in instead. It is indistinguishable from
the real `Set` argument in every dimension the typing rule inspects, so
analysis and execution agree by construction for all eight `in` variants and
every operand shape. A `Set`-typed stand-in would mirror execution more
literally, but the window-definition expressions built by
`getRootActionsNoMakeSet` end up in an executed plan, and a `ColumnSet` with
no set behind it would be dereferenced when such a plan is serialized.
`allow_experimental_analyzer = 1` was fixed for the same root cause in
#114229, whose test is tagged
`no-old-analyzer`; the divergence appeared when `ColumnSet` became always
constant in #100226.
|
Workflow [PR], commit [4d1214f] Summary: ✅
AI ReviewSummaryThis PR fixes the old-analyzer Findings
Final Verdict
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 12/12 (100.00%) · Uncovered code |
Build profile diff (arm_release)Comparing ✅ No significant changes. Binary sizes
Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction. Compile time of recompiled translation units8 translation units recompiled, 15 s compile time in total, 8 of them have a recent master baseline. |
Adding a constant column to stand in for the set that is not built yet broke `arrayFilter(x -> x IN (subquery), arr)`: the stand-in becomes part of the captured arguments of the lambda, and later analysis passes that never created it fail with `Unknown column: __ignored_set` (`02178_column_function_insert_from` in the old-analyzer coverage shard). Take the left operand alone instead. A single argument gives the same result type as the real `in`, whose set argument is always a constant column and so is skipped by the same `LowCardinality` bookkeeping, and it leaves the actions DAG untouched. `inIgnoreSet(x, set)` written explicitly keeps working, hence the variadic arity.
| return function_name; | ||
| } | ||
|
|
||
| /// The `IgnoreSet` variants are called with the left operand alone during type analysis, but |
There was a problem hiding this comment.
appendWindowFunctionsArguments() executes the no_makeset DAG before WindowStep, so this in*IgnoreSet node is not limited to type inference. FunctionIn::executeImpl still returns an all-zero UInt8 column whenever ignore_set is true (src/Functions/in.cpp:101-102), which means enable_analyzer = 0 queries such as SELECT row_number() OVER (PARTITION BY s IN (SELECT 'a') ORDER BY s) ... still collapse every row into the same partition instead of evaluating the real IN key. The one-argument placeholder fixes the LowCardinality type mismatch, but it does not make the executed window path semantically correct. This path needs either a real key computation or a way to keep IgnoreSet out of executable window-expression DAGs.
Related: #114229
Related: #100226
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed
LOGICAL_ERROR: Unexpected return type from tuple. Expected Tuple(..., UInt8). Got Tuple(..., LowCardinality(UInt8))when aLowCardinalitycolumn compared withIN (subquery)is read throughtuple(...)across a subquery boundary andenable_analyzer = 0. With the old analyzer,x IN (subquery)over aLowCardinalitycolumn now returnsLowCardinality(UInt8), the same type asx IN (literal list)and the same as withenable_analyzer = 1.Description
The old-analyzer half of #114229, whose test is tagged
no-old-analyzer.Reproducer, broken since 26.6 and still broken on master:
Root cause. The general typing rule wraps a result in
LowCardinalityonly when at most one argument is a fullLowCardinalitycolumn and no argument is a full ordinary column (IFunctionOverloadResolver::getReturnType,src/Functions/IFunction.cpp:797). The two passes over the same expression disagree:ColumnSetis always wrapped inColumnConstsince Enforce invariant thatActionsDAG::Node::columnis always ColumnConst #100226) and aSet-typed argument counts as constant since TypeIN (subquery)overLowCardinalitythe same asIN (literal list)#114229, soinreturnsLowCardinality(UInt8);inis replaced withinIgnoreSetand the left operand is passed twice as the stand-in for the missing set (src/Interpreters/ActionsVisitor.cpp). Two fullLowCardinalityarguments fail thenum_full_low_cardinality_columns <= 1condition, so the result types as plainUInt8.The outer query records
UInt8for that column and the pipeline deliversLowCardinality(UInt8), socolumnMatchesTyperejects thetupleresult inexecuteActionForPartialResultduringActionsDAG::updateHeader.DESCRIBE (subquery)andEXPLAIN header = 1disagreeing on the type is the quick diagnostic. In a release build this is a query error; in debug and sanitizer builds it aborts the server.Change. The stand-in for the missing set is a constant
UInt8instead of the left operand. It is indistinguishable from the realSetargument in every dimension the typing rule inspects — it is constant, so it is skipped by bothLowCardinalitycounters; it is notNullable, so it does not affect the null wrapper; andFunctionIn::getReturnTypeImplonly readsarguments[0]. Analysis and execution therefore agree by construction, for all eightin/notIn/nullIn/notNullIn×globalvariants and for every operand shape, not only the one in the reproducer. TheIgnoreSetvariants never look at their second argument.A
Set-typed stand-in would mirror execution more literally, butgetRootActionsNoMakeSetfeeds window-definition expressions into an executed plan (src/Interpreters/ExpressionAnalyzer.cpp:1562), which can be serialized for distributed execution, and aColumnSetwith no set behind it would be dereferenced there. AUInt8constant cannot be.Visible type change, mirroring what #114229 did for the analyzer: with
enable_analyzer = 0,toTypeName(x IN (subquery)),DESCRIBEof such a subquery,EXPLAINheaders, and column types ofCREATE ... AS SELECTover such an expression now showLowCardinality(UInt8)instead ofUInt8. Values do not change.Validation. New stateless test
04894_in_subquery_lowcardinality_type_old_analyzerasserts, for both analyzers, that the literal-set and subquery-set forms have the same type, that the type recorded for the subquery column isLowCardinality(UInt8), and thattuple(*)over that column across a subquery boundary works, withIN/NOT INcount()controls. Its first two arms fail and thetuple(*)arm aborts a debug server without the change.Workflow [PR]
Sync PR [sync-upstream/pr/114856]
Version info
26.8.1.1958(included in26.8and later)