[
https://issues.apache.org/jira/browse/HIVE-29598?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Konstantin Bereznyakov updated HIVE-29598:
------------------------------------------
Description:
The vectorizer can assign the same physical `LongColumnVector` slot to two
different logical columns: an intermediate scratch result (e.g. `cast(STRING AS
INT)`) and a small-side broadcast value column carried by a
`VectorMapJoinOuter*Operator`. The intermediate writes a typed value (an
`int`); the broadcast then writes a typed value of a different logical type
(e.g. a `boolean`) to the same slot for matched rows.
For *unmatched* rows the broadcast write is skipped — only `isNull[i]` is
flipped — and the slot retains the stale intermediate value. Any downstream
operator that reads `vector[i]` without first checking `isNull[i]` then
propagates a value of the wrong logical type into the query result. The bug is
silent.
Reproduced on Apache Hive master, commit `6f2355e22c` (`pom.xml` declares
version `4.3.0-SNAPSHOT`). Released 4.x versions sharing this `Vectorizer` /
`VectorMapJoinOuter*Operator` / `ColOrCol` code are likely also exposed but
were not separately verified by this filing.
The repro below uses the projection-mode `ColOrCol` path; analogous
projection-mode operators that read `vector[i]` directly (`ColAndCol`,
long-column arithmetic, equality/comparison expressions,
`VectorUDFAdaptor`-routed UDFs) are exposed by the same root cause but are not
separately verified by this filing.
1. When does the bug fire?
All of the following co-occurring conditions are required:
- Vectorized execution is on (default).
- A vectorized OUTER-join MapJoin runs (`VectorMapJoinOuter*Operator`). INNER
/ LEFT SEMI / ANTI variants do not propagate stale slot data through unmatched
rows.
- The vectorizer aliases a scratch column with a `smallTableValueMapping`
target — typical when a `LongColumnVector`-backed type (e.g. `int`) is computed
for the join-key expression and a `LongColumnVector`-backed broadcast value
(e.g. `boolean`) is also produced from the small side.
- Some output row is unmatched on the join keys, going through
`generateOuterNulls`.
- A downstream operator reads `vector[i]` of the affected slot without
consulting `isNull[i]`.
- The reader's contract is violated by the dirty value (e.g. a strict `== 1`
check fails on a non-` \{0,1}` long).
Removing any one condition masks the bug.
2. Reproduction
{code:java}
SET hive.auto.convert.join=true;
SET hive.auto.convert.join.noconditionaltask=true;
CREATE TABLE t (k STRING, v STRING) STORED AS ORC;
INSERT INTO t VALUES
('A','1'),('A','2'),('A','3'),
('B','2'),('B','3'),
('C','3'),
('D','1'),('D','3');
WITH
probe AS (
SELECT k, v, (CAST(v AS INT) > 0) AS p_bool
FROM t WHERE CAST(v AS INT) >= 3
),
small_side AS (
SELECT k, v, (CAST(v AS INT) > 9999) AS s_bool
FROM t
),
classified AS (
SELECT p.k, p.v, CAST((s.s_bool OR p.p_bool) AS INT) AS observed_value
FROM probe p
LEFT JOIN small_side s
ON p.k = s.k
AND CAST(p.v AS INT) - 1 = CAST(s.v AS INT)
),
diagnosed AS (
–- Aggregate barrier: prevents Calcite from inlining the inner expression
–- back into the outer WHERE and simplifying the bug surface away.
SELECT k, v, MAX(observed_value) AS observed_value
FROM classified
GROUP BY k, v
)
SELECT k, v,
observed_value AS observed_value_returned_by_select,
1 AS required_value_per_sql_semantics
FROM diagnosed
WHERE observed_value = 0
ORDER BY k, v;
{code}
{*}Expected{*}: zero rows. Every probe row's `(s_bool OR p_bool)` is `TRUE` per
SQL semantics (matched: `FALSE OR TRUE`; unmatched: `NULL OR TRUE`), so
`cast(... AS INT) = 1` for every row, and no row can satisfy `WHERE
observed_value = 0`.
{*}Actual{*}:
{code:java}
C 3 0 1
D 3 0 1{code}
Two rows are returned. The filter `WHERE observed_value = 0` accepts them
because their stored `observed_value` is 0. The `1 AS
required_value_per_sql_semantics` projected alongside makes the contradiction
visible per row: SQL semantics demand value 1; the database returned 0.
`C` and `D` are exactly the unmatched probe rows.
3 Workaround
{code:java}
SET hive.vectorized.execution.enabled=false;{code}
Same query then returns zero rows.
4. Mechanism (pointer to the slot reuse)
`EXPLAIN VECTORIZATION DETAIL` of the repro shows physical column
{*}{{*}}`4`{{*}}{*} allocated for two purposes:
{code:java}
bigTableKeyExpressions:
LongColSubtractLongScalar(col 4:int, val 1)(children:
CastStringToLong(col 1:string) -> 4:int) -> 6:int
className: VectorMapJoinOuterMultiKeyOperator
smallTableValueMapping: 4:boolean{code}
For matched rows the broadcast writes a `boolean` (0/1) into `vector[col
4][i]`, overwriting the cast intermediate. For unmatched rows
`generateOuterNulls`
(`ql/.../mapjoin/VectorMapJoinOuterGenerateResultOperator.java`) flips
`isNull[col 4][i] = true` but leaves `vector[col 4][i]` holding the stale `int`
from `CastStringToLong`. A subsequent projection of `cast(s_bool OR p_bool AS
INT)` reads the slot through `ColOrCol` (`ql/.../expressions/ColOrCol.java`)
followed by `IfExprLongScalarLongScalar`, and the dirty value (`stale_int | 1`)
fails the latter's strict `== 1` check, producing 0.
Additional note: disabling CBO (which steers the plan away from this vectorized
path) is also observed to produce correct results, consistent with the analysis
above.
was:
The vectorizer can assign the same physical `LongColumnVector` slot to two
different logical columns: an intermediate scratch result (e.g. `cast(STRING AS
INT)`) and a small-side broadcast value column carried by a
`VectorMapJoinOuter*Operator`. The intermediate writes a typed value (an
`int`); the broadcast then writes a typed value of a different logical type
(e.g. a `boolean`) to the same slot for matched rows.
For *unmatched* rows the broadcast write is skipped — only `isNull[i]` is
flipped — and the slot retains the stale intermediate value. Any downstream
operator that reads `vector[i]` without first checking `isNull[i]` then
propagates a value of the wrong logical type into the query result. The bug is
silent.
Reproduced on Apache Hive master, commit `6f2355e22c` (`pom.xml` declares
version `4.3.0-SNAPSHOT`). Released 4.x versions sharing this `Vectorizer` /
`VectorMapJoinOuter*Operator` / `ColOrCol` code are likely also exposed but
were not separately verified by this filing.
The repro below uses the projection-mode `ColOrCol` path; analogous
projection-mode operators that read `vector[i]` directly (`ColAndCol`,
long-column arithmetic, equality/comparison expressions,
`VectorUDFAdaptor`-routed UDFs) are exposed by the same root cause but are not
separately verified by this filing.
1. When does the bug fire?
All of the following co-occurring conditions are required:
- Vectorized execution is on (default).
- A vectorized OUTER-join MapJoin runs (`VectorMapJoinOuter*Operator`). INNER
/ LEFT SEMI / ANTI variants do not propagate stale slot data through unmatched
rows.
- The vectorizer aliases a scratch column with a `smallTableValueMapping`
target — typical when a `LongColumnVector`-backed type (e.g. `int`) is computed
for the join-key expression and a `LongColumnVector`-backed broadcast value
(e.g. `boolean`) is also produced from the small side.
- Some output row is unmatched on the join keys, going through
`generateOuterNulls`.
- A downstream operator reads `vector[i]` of the affected slot without
consulting `isNull[i]`.
- The reader's contract is violated by the dirty value (e.g. a strict `== 1`
check fails on a non-` \{0,1}` long).
Removing any one condition masks the bug.
2. Reproduction
{code:java}
SET hive.auto.convert.join=true; – default true
SET hive.auto.convert.join.noconditionaltask=true; – default true
CREATE TABLE t (k STRING, v STRING) STORED AS ORC;
INSERT INTO t VALUES
('A','1'),('A','2'),('A','3'),
('B','2'),('B','3'),
('C','3'),
('D','1'),('D','3');
WITH
probe AS (
SELECT k, v, (CAST(v AS INT) > 0) AS p_bool
FROM t WHERE CAST(v AS INT) >= 3
),
small_side AS (
SELECT k, v, (CAST(v AS INT) > 9999) AS s_bool
FROM t
),
classified AS (
SELECT p.k, p.v, CAST((s.s_bool OR p.p_bool) AS INT) AS observed_value
FROM probe p
LEFT JOIN small_side s
ON p.k = s.k
AND CAST(p.v AS INT) - 1 = CAST(s.v AS INT)
),
diagnosed AS (
– Aggregate barrier: prevents Calcite from inlining the inner expression
– back into the outer WHERE and simplifying the bug surface away.
SELECT k, v, MAX(observed_value) AS observed_value
FROM classified
GROUP BY k, v
)
SELECT k, v,
observed_value AS observed_value_returned_by_select,
1 AS required_value_per_sql_semantics
FROM diagnosed
WHERE observed_value = 0
ORDER BY k, v;
{code}
{*}Expected{*}: zero rows. Every probe row's `(s_bool OR p_bool)` is `TRUE` per
SQL semantics (matched: `FALSE OR TRUE`; unmatched: `NULL OR TRUE`), so
`cast(... AS INT) = 1` for every row, and no row can satisfy `WHERE
observed_value = 0`.
{*}Actual{*}:
{code:java}
C 3 0 1
D 3 0 1{code}
Two rows are returned. The filter `WHERE observed_value = 0` accepts them
because their stored `observed_value` is 0. The `1 AS
required_value_per_sql_semantics` projected alongside makes the contradiction
visible per row: SQL semantics demand value 1; the database returned 0.
`C` and `D` are exactly the unmatched probe rows.
3 Workaround
{code:java}
SET hive.vectorized.execution.enabled=false;{code}
Same query then returns zero rows.
4. Mechanism (pointer to the slot reuse)
`EXPLAIN VECTORIZATION DETAIL` of the repro shows physical column *{*}`4`{*}*
allocated for two purposes:
{code:java}
bigTableKeyExpressions:
LongColSubtractLongScalar(col 4:int, val 1)(children:
CastStringToLong(col 1:string) -> 4:int) -> 6:int
className: VectorMapJoinOuterMultiKeyOperator
smallTableValueMapping: 4:boolean{code}
For matched rows the broadcast writes a `boolean` (0/1) into `vector[col
4][i]`, overwriting the cast intermediate. For unmatched rows
`generateOuterNulls`
(`ql/.../mapjoin/VectorMapJoinOuterGenerateResultOperator.java`) flips
`isNull[col 4][i] = true` but leaves `vector[col 4][i]` holding the stale `int`
from `CastStringToLong`. A subsequent projection of `cast(s_bool OR p_bool AS
INT)` reads the slot through `ColOrCol` (`ql/.../expressions/ColOrCol.java`)
followed by `IfExprLongScalarLongScalar`, and the dirty value (`stale_int | 1`)
fails the latter's strict `== 1` check, producing 0.
Additional note: disabling CBO (which steers the plan away from this vectorized
path) is also observed to produce correct results, consistent with the analysis
above.
> Vectorization: Scratch column reuse sometimes produces wrong results
> --------------------------------------------------------------------
>
> Key: HIVE-29598
> URL: https://issues.apache.org/jira/browse/HIVE-29598
> Project: Hive
> Issue Type: Bug
> Reporter: Konstantin Bereznyakov
> Priority: Major
>
> The vectorizer can assign the same physical `LongColumnVector` slot to two
> different logical columns: an intermediate scratch result (e.g. `cast(STRING
> AS INT)`) and a small-side broadcast value column carried by a
> `VectorMapJoinOuter*Operator`. The intermediate writes a typed value (an
> `int`); the broadcast then writes a typed value of a different logical type
> (e.g. a `boolean`) to the same slot for matched rows.
> For *unmatched* rows the broadcast write is skipped — only `isNull[i]` is
> flipped — and the slot retains the stale intermediate value. Any downstream
> operator that reads `vector[i]` without first checking `isNull[i]` then
> propagates a value of the wrong logical type into the query result. The bug
> is silent.
> Reproduced on Apache Hive master, commit `6f2355e22c` (`pom.xml` declares
> version `4.3.0-SNAPSHOT`). Released 4.x versions sharing this `Vectorizer` /
> `VectorMapJoinOuter*Operator` / `ColOrCol` code are likely also exposed but
> were not separately verified by this filing.
> The repro below uses the projection-mode `ColOrCol` path; analogous
> projection-mode operators that read `vector[i]` directly (`ColAndCol`,
> long-column arithmetic, equality/comparison expressions,
> `VectorUDFAdaptor`-routed UDFs) are exposed by the same root cause but are
> not separately verified by this filing.
> 1. When does the bug fire?
> All of the following co-occurring conditions are required:
> - Vectorized execution is on (default).
> - A vectorized OUTER-join MapJoin runs (`VectorMapJoinOuter*Operator`).
> INNER / LEFT SEMI / ANTI variants do not propagate stale slot data through
> unmatched rows.
> - The vectorizer aliases a scratch column with a `smallTableValueMapping`
> target — typical when a `LongColumnVector`-backed type (e.g. `int`) is
> computed for the join-key expression and a `LongColumnVector`-backed
> broadcast value (e.g. `boolean`) is also produced from the small side.
> - Some output row is unmatched on the join keys, going through
> `generateOuterNulls`.
> - A downstream operator reads `vector[i]` of the affected slot without
> consulting `isNull[i]`.
> - The reader's contract is violated by the dirty value (e.g. a strict `== 1`
> check fails on a non-` \{0,1}` long).
> Removing any one condition masks the bug.
> 2. Reproduction
> {code:java}
> SET hive.auto.convert.join=true;
> SET hive.auto.convert.join.noconditionaltask=true;
> CREATE TABLE t (k STRING, v STRING) STORED AS ORC;
> INSERT INTO t VALUES
> ('A','1'),('A','2'),('A','3'),
> ('B','2'),('B','3'),
> ('C','3'),
> ('D','1'),('D','3');
> WITH
> probe AS (
> SELECT k, v, (CAST(v AS INT) > 0) AS p_bool
> FROM t WHERE CAST(v AS INT) >= 3
> ),
> small_side AS (
> SELECT k, v, (CAST(v AS INT) > 9999) AS s_bool
> FROM t
> ),
> classified AS (
> SELECT p.k, p.v, CAST((s.s_bool OR p.p_bool) AS INT) AS observed_value
> FROM probe p
> LEFT JOIN small_side s
> ON p.k = s.k
> AND CAST(p.v AS INT) - 1 = CAST(s.v AS INT)
> ),
> diagnosed AS (
> –- Aggregate barrier: prevents Calcite from inlining the inner expression
> –- back into the outer WHERE and simplifying the bug surface away.
> SELECT k, v, MAX(observed_value) AS observed_value
> FROM classified
> GROUP BY k, v
> )
> SELECT k, v,
> observed_value AS observed_value_returned_by_select,
> 1 AS required_value_per_sql_semantics
> FROM diagnosed
> WHERE observed_value = 0
> ORDER BY k, v;
> {code}
> {*}Expected{*}: zero rows. Every probe row's `(s_bool OR p_bool)` is `TRUE`
> per SQL semantics (matched: `FALSE OR TRUE`; unmatched: `NULL OR TRUE`), so
> `cast(... AS INT) = 1` for every row, and no row can satisfy `WHERE
> observed_value = 0`.
> {*}Actual{*}:
> {code:java}
> C 3 0 1
> D 3 0 1{code}
> Two rows are returned. The filter `WHERE observed_value = 0` accepts them
> because their stored `observed_value` is 0. The `1 AS
> required_value_per_sql_semantics` projected alongside makes the contradiction
> visible per row: SQL semantics demand value 1; the database returned 0.
> `C` and `D` are exactly the unmatched probe rows.
> 3 Workaround
> {code:java}
> SET hive.vectorized.execution.enabled=false;{code}
> Same query then returns zero rows.
> 4. Mechanism (pointer to the slot reuse)
> `EXPLAIN VECTORIZATION DETAIL` of the repro shows physical column
> {*}{{*}}`4`{{*}}{*} allocated for two purposes:
> {code:java}
> bigTableKeyExpressions:
> LongColSubtractLongScalar(col 4:int, val 1)(children:
> CastStringToLong(col 1:string) -> 4:int) -> 6:int
> className: VectorMapJoinOuterMultiKeyOperator
> smallTableValueMapping: 4:boolean{code}
> For matched rows the broadcast writes a `boolean` (0/1) into `vector[col
> 4][i]`, overwriting the cast intermediate. For unmatched rows
> `generateOuterNulls`
> (`ql/.../mapjoin/VectorMapJoinOuterGenerateResultOperator.java`) flips
> `isNull[col 4][i] = true` but leaves `vector[col 4][i]` holding the stale
> `int` from `CastStringToLong`. A subsequent projection of `cast(s_bool OR
> p_bool AS INT)` reads the slot through `ColOrCol`
> (`ql/.../expressions/ColOrCol.java`) followed by
> `IfExprLongScalarLongScalar`, and the dirty value (`stale_int | 1`) fails the
> latter's strict `== 1` check, producing 0.
> Additional note: disabling CBO (which steers the plan away from this
> vectorized path) is also observed to produce correct results, consistent with
> the analysis above.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)