in core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java [7325:7629]
throw newValidationError(
literal, RESOURCE.orderByOrdinalOutOfRange());
}
// SQL ordinals are 1-based, but Sort's are 0-based
int ordinal = intValue - 1;
return nthSelectItem(ordinal, literal.getParserPosition());
}
break;
default:
break;
}
}
return super.visit(literal);
}
/**
* Returns the <code>ordinal</code>th item in the select list.
*/
private SqlNode nthSelectItem(int ordinal, final SqlParserPos pos) {
// TODO: Don't expand the list every time. Maybe keep an expanded
// version of each expression -- select lists and identifiers -- in
// the validator.
SqlNodeList expandedSelectList =
expandStar(
SqlNonNullableAccessors.getSelectList(select),
select,
false);
SqlNode expr = expandedSelectList.get(ordinal);
expr = stripAs(expr);
if (expr instanceof SqlIdentifier) {
expr = getScope().fullyQualify((SqlIdentifier) expr).identifier;
}
// Create a copy of the expression with the position of the order
// item.
return expr.clone(pos);
}
@Override public SqlNode visit(SqlIdentifier id) {
// Aliases, e.g. 'select a as x, b from t order by x'.
if (id.isSimple()
&& config.conformance().isSortByAlias()) {
String alias = id.getSimple();
final SqlValidatorNamespace selectNs = getNamespaceOrThrow(select);
final RelDataType rowType =
selectNs.getRowTypeSansSystemColumns();
final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
RelDataTypeField field = nameMatcher.field(rowType, alias);
if (field != null) {
return nthSelectItem(
field.getIndex(),
id.getParserPosition());
}
}
// No match. Return identifier unchanged.
return getScope().fullyQualify(id).identifier;
}
@Override protected @Nullable SqlNode visitScoped(SqlCall call) {
// Don't attempt to expand sub-queries. We haven't implemented
// these yet.
if (call instanceof SqlSelect) {
return call;
}
return super.visitScoped(call);
}
}
/**
* Converts an expression into canonical form by fully-qualifying any
* identifiers. For common columns in USING, it will be converted to
* COALESCE(A.col, B.col) AS col. When using a conformance that
* allows isSelectAlias, it expands identifiers that refer to
* local select expressions into the expressions themselves.
*/
static class SelectExpander extends Expander {
final SqlSelect select;
// Maps simple identifiers to their expansions.
final Map<String, SqlNode> expansions;
// List of identifiers that are currently being looked-up. Used to detect
// circular dependencies when SqlConformance#isSelectAlias is ANY.
final Set<String> lookingUp;
@Nullable SqlNode root = null;
private SelectExpander(SqlValidatorImpl validator, SelectScope scope,
SqlSelect select, Map<String, SqlNode> expansions, Set<String> lookingUp) {
super(validator, scope);
this.select = select;
this.expansions = expansions;
this.lookingUp = lookingUp;
}
/**
* Creates an expander for the items in a SELECT list.
*
* @param validator Validator to use.
* @param scope Scope to lookup identifiers in.
* @param select Select that is being expanded.
* @param expansions List of expansions computed for identifiers that are
* defined in the current select list and also used in the select list.
* Only used if {@link SqlConformance#isSelectAlias()} requires it.
*/
private SelectExpander(SqlValidatorImpl validator, SelectScope scope,
SqlSelect select, Map<String, SqlNode> expansions) {
this(validator, scope, select, expansions, new HashSet<>());
}
private SelectExpander(SelectExpander expander) {
this(expander.validator, (SelectScope) expander.getScope(),
expander.select, expander.expansions, expander.lookingUp);
}
@Override public SqlNode go(SqlNode root) {
this.root = root;
return super.go(root);
}
@Override public @Nullable SqlNode visit(SqlIdentifier id) {
final SqlNode node =
expandCommonColumn(select, id, (SelectScope) getScope(), validator);
if (node != id) {
return node;
} else {
try {
return super.visit(id);
} catch (Exception ex) {
if (!(ex instanceof CalciteContextException)) {
throw ex;
}
String message = ex.getMessage();
if (message != null && !message.contains("not found")) {
throw ex;
}
// This point is reached only if the name lookup failed using standard rules
SqlConformance.SelectAliasLookup selectAlias = validator.getConformance().isSelectAlias();
if (selectAlias == SqlConformance.SelectAliasLookup.UNSUPPORTED || !id.isSimple()) {
throw ex;
}
return expandAliases(id, (CalciteContextException) ex);
// end of catch block
}
}
}
// Handle "SELECT expr as X, X+1 as Y"
// where X is not defined previously.
// Try to look up the item in the select list itself.
private SqlNode expandAliases(SqlIdentifier id, CalciteContextException ex) {
String name = id.getSimple();
if (lookingUp.contains(name)) {
final String dependentColumns = lookingUp.stream()
.map(s -> "'" + s + "'")
.collect(joining(", "));
throw validator.newValidationError(id,
RESOURCE.columnIsCyclic(name, dependentColumns));
}
SqlConformance.SelectAliasLookup selectAlias = validator.getConformance().isSelectAlias();
// Check whether we already have an expansion for this identifier
@Nullable SqlNode expr = null;
if (expansions.containsKey(name)) {
expr = expansions.get(name);
} else {
final SqlNameMatcher nameMatcher =
validator.catalogReader.nameMatcher();
int matches = 0;
for (SqlNode s : select.getSelectList()) {
if (s == this.root && selectAlias == SqlConformance.SelectAliasLookup.LEFT_TO_RIGHT) {
// Stop lookup at the current item
break;
}
final String alias = SqlValidatorUtil.alias(s);
if (alias != null && nameMatcher.matches(alias, name)) {
expr = s;
matches++;
}
}
if (matches == 0) {
// Throw the original exception
throw ex;
} else if (matches > 1) {
// More than one column has this alias.
throw validator.newValidationError(id,
RESOURCE.columnAmbiguous(name));
}
expr = stripAs(requireNonNull(expr, "expr"));
}
// Recursively expand the result
lookingUp.add(name);
SelectExpander expander = new SelectExpander(this);
final SqlNode newExpr = expander.go(expr);
lookingUp.remove(name);
if (expr != newExpr) {
validator.setOriginal(newExpr, expr);
}
expr = newExpr;
if (expr instanceof SqlIdentifier) {
expr = getScope().fullyQualify((SqlIdentifier) expr).identifier;
}
if (!expansions.containsKey(name)) {
expansions.put(name, expr);
validator.setOriginal(expr, id);
}
return expr;
}
}
/**
* Shuttle which walks over an expression in the GROUP BY/HAVING clause, replacing
* usages of aliases or ordinals with the underlying expression.
*/
static class ExtendedExpander extends Expander {
final SqlSelect select;
final SqlNode root;
final Clause clause;
// Retain only expandable aliases or ordinals to prevent their expansion in a SQL call expr.
final Set<SqlNode> aliasOrdinalExpandSet = Sets.newIdentityHashSet();
ExtendedExpander(SqlValidatorImpl validator, SqlValidatorScope scope,
SqlSelect select, SqlNode root, Clause clause) {
super(validator, scope);
this.select = select;
this.root = root;
this.clause = clause;
if (clause == Clause.GROUP_BY) {
addExpandableExpressions();
}
}
@Override public @Nullable SqlNode visit(SqlIdentifier id) {
if (!id.isSimple()) {
return super.visit(id);
}
final SelectScope selectScope = validator.getRawSelectScopeNonNull(select);
final boolean replaceAliases = clause.shouldReplaceAliases(validator.config);
if (!replaceAliases || (clause == Clause.GROUP_BY && !aliasOrdinalExpandSet.contains(id))) {
SqlNode node = expandCommonColumn(select, id, selectScope, validator);
if (node != id) {
return node;
}
return super.visit(id);
}
String name = id.getSimple();
SqlNode expr = null;
final SqlNameMatcher nameMatcher =
validator.catalogReader.nameMatcher();
int n = 0;
for (SqlNode s : SqlNonNullableAccessors.getSelectList(select)) {
final @Nullable String alias = SqlValidatorUtil.alias(s);
if (alias != null && nameMatcher.matches(alias, name)) {
expr = s;
n++;
}
}
if (n == 0) {
return super.visit(id);
} else if (n > 1) {
// More than one column has this alias.
throw validator.newValidationError(id,
RESOURCE.columnAmbiguous(name));
}
Iterable<SqlCall> allAggList = validator.aggFinder.findAll(ImmutableList.of(root));
for (SqlCall agg : allAggList) {
if (clause == Clause.HAVING && containsIdentifier(agg, id)) {
return super.visit(id);
}
}
// expr cannot be null; in that case n = 0 would have returned
requireNonNull(expr, "expr");
if (validator.getConformance().isSelectAlias()
!= SqlConformance.SelectAliasLookup.UNSUPPORTED) {
Map<String, SqlNode> expansions = new HashMap<>();
final Expander expander = new SelectExpander(validator, selectScope, select, expansions);
expr = expander.go(expr);
}
expr = stripAs(expr);
if (expr instanceof SqlIdentifier) {
SqlIdentifier sid = (SqlIdentifier) expr;
final SqlIdentifier fqId = getScope().fullyQualify(sid).identifier;
expr = expandDynamicStar(sid, fqId);
}
return expr;
}
@Override public @Nullable SqlNode visit(SqlLiteral literal) {
if (clause != Clause.GROUP_BY
|| !validator.config().conformance().isGroupByOrdinal()) {
return super.visit(literal);
}
boolean isOrdinalLiteral = aliasOrdinalExpandSet.contains(literal);
if (isOrdinalLiteral) {
switch (literal.getTypeName()) {
case DECIMAL:
case DOUBLE:
final int intValue = literal.intValue(false);
if (intValue >= 0) {
if (intValue < 1 || intValue > SqlNonNullableAccessors.getSelectList(select).size()) {