Skip to content
20 changes: 20 additions & 0 deletions ql/src/java/org/apache/hadoop/hive/ql/exec/ColumnInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public class ColumnInfo implements Serializable {

private boolean isHiddenVirtualCol;

/**
* Deliberately excluded from equals/hashCode/isSameColumnForRR: a marked and an unmarked
* copy of a column are still the same column for RowResolver purposes (HIVE-29580).
*/
private boolean ambiguousName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we add a note saying that ambiguousName is intentionally excluded from equals and hashcode? Also add a brief reasoning for future devs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a note on the field: it is deliberately excluded from equals/hashCode/isSameColumnForRR — a marked and an unmarked copy of a column are still the same column for RowResolver purposes, so including the flag would change RR dedup semantics.


private String typeName;

private final boolean nullable;
Expand Down Expand Up @@ -130,9 +136,23 @@ public ColumnInfo(ColumnInfo columnInfo) {
this.isVirtualCol = columnInfo.getIsVirtualCol();
this.isHiddenVirtualCol = columnInfo.isHiddenVirtualCol();
this.nullable = columnInfo.nullable;
this.ambiguousName = columnInfo.ambiguousName;
this.setType(columnInfo.getType());
}

/**
* True when this column's alias collided with another column's at a subquery/CTE boundary:
* the column stays usable positionally (star expansion, count(*)) but a user-written by-name
* reference is ambiguous and must be rejected.
*/
public boolean hasAmbiguousName() {
return ambiguousName;
}

public void setAmbiguousName(boolean ambiguousName) {
this.ambiguousName = ambiguousName;
}

public String getTypeName() {
return this.typeName;
}
Expand Down
46 changes: 46 additions & 0 deletions ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -3610,13 +3610,26 @@ private RelNode genGBLogicalPlan(QB qb, RelNode srcRel) throws SemanticException
// SEL%SEL% rule.
ASTNode selExprList = qb.getParseInfo().getSelForClause(destClauseName);
SubQueryUtils.checkForTopLevelSubqueries(selExprList);
List<Integer> clearedAmbiguousPositions = new ArrayList<>();
List<ColumnInfo> clearedAmbiguousColumns = null;
if (selExprList.getToken().getType() == HiveParser.TOK_SELECTDI
&& selExprList.getChildCount() == 1 && selExprList.getChild(0).getChildCount() == 1) {
ASTNode node = (ASTNode) selExprList.getChild(0).getChild(0);
if (node.getToken().getType() == HiveParser.TOK_ALLCOLREF) {
// As we said before, here we use genSelectLogicalPlan to rewrite AllColRef
srcRel = genSelectLogicalPlan(qb, srcRel, srcRel, null, null, true).getKey();
RowResolver rr = relToHiveRR.get(srcRel);
// Clear the HIVE-29580 ambiguity markers on this rewrite-private projection (its
// ColumnInfos are genColListRegex copies; the subquery's own RowResolver keeps its
// markers) so genSelectDIAST's synthesized by-name references type check; reapplied
// to the group by output below so that references crossing this boundary still fail.
clearedAmbiguousColumns = rr.getColumnInfos();
for (int i = 0; i < clearedAmbiguousColumns.size(); i++) {
if (clearedAmbiguousColumns.get(i).hasAmbiguousName()) {
clearedAmbiguousPositions.add(i);
clearedAmbiguousColumns.get(i).setAmbiguousName(false);
}
}
qbp.setSelExprForClause(destClauseName, genSelectDIAST(rr));
}
}
Expand Down Expand Up @@ -3757,6 +3770,16 @@ private RelNode genGBLogicalPlan(QB qb, RelNode srcRel) throws SemanticException
groupByRel = genGBRelNode(groupByExpressions, aggregations, groupingSets, srcRel);
relToHiveColNameCalcitePosMap.put(groupByRel, buildHiveToCalciteColumnMap(groupByOutputRowResolver));
relToHiveRR.put(groupByRel, groupByOutputRowResolver);
// Reapply the markers cleared for the DISTINCT * rewrite above; its group by output is
// positionally one key per input column (no aggregations, no grouping sets). Copy the
// user-visible names too, so a rejection resolving against this exprResolver RR (e.g.
// a HAVING reference) reads "c in t" rather than the expression tree.
for (int position : clearedAmbiguousPositions) {
ColumnInfo gbColInfo = groupByOutputRowResolver.getColumnInfos().get(position);
gbColInfo.setAmbiguousName(true);
gbColInfo.setAlias(clearedAmbiguousColumns.get(position).getAlias());
gbColInfo.setTabAlias(clearedAmbiguousColumns.get(position).getTabAlias());
}
}

return groupByRel;
Expand Down Expand Up @@ -4429,6 +4452,13 @@ && isRegex(
ColumnInfo colInfo = new ColumnInfo(SemanticAnalyzer.getColumnInternalName(pos),
TypeInfoUtils.getStandardWritableObjectInspectorFromTypeInfo(typeInfo),
tabAlias, false);
if (expression instanceof RexInputRef inputRef) {
// Carry the HIVE-29580 marker through this projection. Only expression-map
// resolutions (the DISTINCT * rewrite's synthesized references) reach here with a
// marked source; user-written ones already failed checkAmbiguousName in genRexNode.
ColumnInfo sourceColInfo = inputRR.getColumnInfos().get(inputRef.getIndex());
colInfo.setAmbiguousName(sourceColInfo.hasAmbiguousName());
}
outputRR.put(tabAlias, colAlias, colInfo);

pos = Integer.valueOf(pos.intValue() + 1);
Expand Down Expand Up @@ -4546,6 +4576,7 @@ && isRegex(
ColumnInfo colInfo = outputRR.getColumnInfos().get(i);
ColumnInfo newColInfo = new ColumnInfo(colInfo.getInternalName(),
colInfo.getType(), colInfo.getTabAlias(), colInfo.getIsVirtualCol());
newColInfo.setAmbiguousName(colInfo.hasAmbiguousName());
groupByOutputRowResolver.put(colInfo.getTabAlias(), colInfo.getAlias(), newColInfo);
if (gbyKeyExpressions != null && gbyKeyExpressions.size() == outputRR.getColumnInfos().size()) {
groupByOutputRowResolver.putExpression(gbyKeyExpressions.get(i), colInfo);
Expand Down Expand Up @@ -4883,11 +4914,26 @@ private RelNode genLogicalPlan(QB qb, boolean outerMostQB,
ColumnInfo newCi = new ColumnInfo(colInfo);
newCi.setTabAlias(alias);
if (i < targetColNames.size()) {
// An explicit column list disambiguates positionally; a collision with an unlisted
// column is re-marked when that later column's turn reaches the branch below.
tmp[1] = targetColNames.get(i);
newCi.setAlias(tmp[1]);
newCi.setAmbiguousName(false);
} else if ("".equals(tmp[0]) || tmp[1] == null) {
// ast expression is not a valid column name for table
tmp[1] = colInfo.getInternalName();
} else {
ColumnInfo clashingColInfo = newRR.get(alias, tmp[1]);
if (clashingColInfo != null) {
// Duplicate alias escaping the subquery boundary: tolerated for positional use
// (HIVE-19770), but poison the name so a later by-name reference fails (HIVE-29580).
// Binding the duplicate to its internal name here is deliberate, not redundant:
// putWithCheck would otherwise do it via its own fallback AND call keepAmbiguousInfo,
// whose reference-time throw in RowResolver.get would then shadow this marker with a
// differently formatted message. Do not "simplify" this line away.
clashingColInfo.setAmbiguousName(true);
tmp[1] = colInfo.getInternalName();
}
}
newRR.putWithCheck(alias, tmp[1], colInfo.getInternalName(), newCi);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4015,6 +4015,7 @@ Integer genColListRegex(String colRegex, String tabAlias, ASTNode sel,
colList.add(Pair.of(colInfo, colSrcRR));
oColInfo = new ColumnInfo(getColumnInternalName(pos), colInfo.getType(),
colInfo.getTabAlias(), colInfo.getIsVirtualCol(), colInfo.isHiddenVirtualCol());
oColInfo.setAmbiguousName(colInfo.hasAmbiguousName());
inputColsProcessed.put(colInfo, oColInfo);
}
if (ensureUniqueCols) {
Expand Down Expand Up @@ -4102,6 +4103,7 @@ Integer genColListRegex(String colRegex, String tabAlias, ASTNode sel,
colList.add(Pair.of(colInfo, input));
oColInfo = new ColumnInfo(getColumnInternalName(pos), colInfo.getType(),
colInfo.getTabAlias(), colInfo.getIsVirtualCol(), colInfo.isHiddenVirtualCol());
oColInfo.setAmbiguousName(colInfo.hasAmbiguousName());
inputColsProcessed.put(colInfo, oColInfo);
}
assert nonNull(tmp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ private void handleSource(boolean hasWhenNotMatchedClause, String sourceAlias, S
sqlGenerator.append("FROM\n");
sqlGenerator.append("(SELECT ");
sqlGenerator.appendAcidSelectColumns(Operation.MERGE);
sqlGenerator.appendAllColsOfTargetTable();
if (sqlGenerator.getTargetTable().hasNonNativePartitionSupport()) {
// non-native partition columns are ordinary data columns of the row schema
sqlGenerator.appendAllColsOfTargetTable();
} else {
// native partition columns were already emitted by appendAcidSelectColumns above
sqlGenerator.appendNonPartitionColsOfTargetTable();
}
addSourceColumnsForRowLineage(isRowLineageSupported, sqlGenerator, "", conf);
sqlGenerator.append(" FROM ").appendTargetTableName().append(") ");
sqlGenerator.appendSubQueryAlias();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ public void appendAllColsOfTargetTable(String prefix) {
public void appendAllColsOfTargetTable() {
appendCols(targetTable.getAllCols(), FieldSchema::getName);
}

/**
* Appends the target table's non-partition columns. For a natively partitioned MERGE target
* the partition columns are already emitted by appendAcidSelectColumns; emitting them again
* would give the rewritten projection duplicate column names (HIVE-29580).
*/
public void appendNonPartitionColsOfTargetTable() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Consider renaming this method as the name suggests "non-partition columns" whereas we do append "all columns" sometimes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed the name lied for the non-native arm — but renaming would have meant naming a mode branch that really belongs to the caller, so I dissolved it instead: MergeRewriter now branches (appendAllColsOfTargetTable for non-native tables, whose partition columns are ordinary data columns; appendNonPartitionColsOfTargetTable for native ones, whose partition columns were already emitted by appendAcidSelectColumns), and appendNonPartitionColsOfTargetTable shrank to the one line its name promises.

appendCols(targetTable.getCols(), FieldSchema::getName);
}

public <T> void appendCols(List<T> columns, Function<T, String> stringConverter) {
appendCols(columns, null, null, stringConverter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public Object process(Node nd, Stack<Node> stack, NodeProcessorCtx procCtx,
if (!qualifiedAccess) {
colInfo = getColInfo(ctx, null, tableOrCol, expr);
// It's a column.
checkAmbiguousName(colInfo);
return exprFactory.createColumnRefExpr(colInfo, ctx.getInputRRList());
} else if (hasTableAlias(ctx, tableOrCol, expr)) {
return null;
Expand Down Expand Up @@ -180,6 +181,7 @@ protected T processQualifiedColRef(TypeCheckCtx ctx, ASTNode expr,
ErrorMsg.INVALID_COLUMN.getMsg(), expr.getChild(1)), expr);
return null;
}
checkAmbiguousName(colInfo);
ColumnInfo newColumnInfo = new ColumnInfo(colInfo);
newColumnInfo.setTabAlias(tableAlias);
List<RowResolver> listRR = new ArrayList<>(jctx.getInputRRList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,19 @@ protected IntervalExprProcessor getIntervalExprProcessor() {
return new IntervalExprProcessor();
}

/**
* Rejects a by-name reference to a column marked ambiguous at a subquery/CTE boundary
* (HIVE-29580). Call after each by-name resolution of a user-written column reference;
* expression-map resolutions (processGByExpr) stay unchecked so Hive's own rewrites can
* reference marked columns.
*/
static void checkAmbiguousName(ColumnInfo colInfo) throws SemanticException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please document when this method should be called.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a javadoc: call after each by-name resolution of a user-written column reference; expression-map resolutions (processGByExpr) deliberately stay unchecked so Hive's own rewrites (e.g. genSelectDIAST) can reference marked columns.

if (colInfo != null && colInfo.hasAmbiguousName()) {
throw new SemanticException(ErrorMsg.AMBIGUOUS_COLUMN.getMsg(
colInfo.getAlias() + " in " + colInfo.getTabAlias()));
}
}

/**
* Processor for table columns.
*/
Expand Down Expand Up @@ -660,6 +673,7 @@ public Object process(Node nd, Stack<Node> stack, NodeProcessorCtx procCtx,
return null;
}
// It's a column.
checkAmbiguousName(colInfo);
return exprFactory.toExpr(colInfo, usedRR, offset);
} else {
// It's a table alias.
Expand Down Expand Up @@ -694,6 +708,7 @@ public Object process(Node nd, Stack<Node> stack, NodeProcessorCtx procCtx,
}
} else {
// It's a column.
checkAmbiguousName(colInfo);
return exprFactory.toExpr(colInfo, usedRR, offset);
}
}
Expand Down Expand Up @@ -1300,6 +1315,7 @@ protected T processQualifiedColRef(TypeCheckCtx ctx, ASTNode expr,
ErrorMsg.INVALID_COLUMN.getMsg(), expr.getChild(1)), expr);
return null;
}
checkAmbiguousName(colInfo);
return exprFactory.toExpr(colInfo, usedRR, offset);
}

Expand Down
40 changes: 40 additions & 0 deletions ql/src/test/org/apache/hadoop/hive/ql/exec/TestColumnInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.hadoop.hive.ql.exec;

import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory;
import org.junit.Assert;
import org.junit.Test;

public class TestColumnInfo {

@Test
public void testAmbiguousNameDefaultsToFalse() {
ColumnInfo colInfo = new ColumnInfo("_col0", TypeInfoFactory.stringTypeInfo, "t", false);
Assert.assertFalse(colInfo.hasAmbiguousName());
}

@Test
public void testCopyConstructorPreservesAmbiguousName() {
ColumnInfo original = new ColumnInfo("_col0", TypeInfoFactory.stringTypeInfo, "t", false);
original.setAmbiguousName(true);
Assert.assertTrue(new ColumnInfo(original).hasAmbiguousName());
}
}
Loading
Loading