| /* |
| * 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.calcite.sql.validate; |
| |
| import org.apache.calcite.sql.SqlCall; |
| import org.apache.calcite.sql.SqlIdentifier; |
| import org.apache.calcite.sql.SqlKind; |
| import org.apache.calcite.sql.SqlNode; |
| import org.apache.calcite.sql.SqlSelect; |
| import org.apache.calcite.sql.SqlWindow; |
| import org.apache.calcite.sql.fun.SqlStdOperatorTable; |
| import org.apache.calcite.sql.util.SqlBasicVisitor; |
| import org.apache.calcite.util.Litmus; |
| |
| import com.google.common.collect.Iterables; |
| |
| import java.util.ArrayDeque; |
| import java.util.Deque; |
| import java.util.List; |
| |
| import static org.apache.calcite.util.Static.RESOURCE; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| /** |
| * Visitor which throws an exception if any component of the expression is not a |
| * group expression. |
| */ |
| class AggChecker extends SqlBasicVisitor<Void> { |
| //~ Instance fields -------------------------------------------------------- |
| |
| private final Deque<SqlValidatorScope> scopes = new ArrayDeque<>(); |
| private final List<SqlNode> extraExprs; |
| private final List<SqlNode> measureExprs; |
| private final List<SqlNode> groupExprs; |
| private final boolean distinct; |
| private final SqlValidatorImpl validator; |
| |
| //~ Constructors ----------------------------------------------------------- |
| |
| /** |
| * Creates an AggChecker. |
| * |
| * @param validator Validator |
| * @param scope Scope |
| * @param extraExprs Expressions in GROUP BY (or SELECT DISTINCT) clause, |
| * that are therefore available |
| * @param measureExprs Expressions that are the names of measures |
| * @param groupExprs Expressions in GROUP BY (or SELECT DISTINCT) clause, |
| * that are therefore available |
| * @param distinct Whether aggregation checking is because of a SELECT |
| * DISTINCT clause |
| */ |
| AggChecker( |
| SqlValidatorImpl validator, |
| AggregatingScope scope, |
| List<SqlNode> extraExprs, |
| List<SqlNode> measureExprs, |
| List<SqlNode> groupExprs, |
| boolean distinct) { |
| this.validator = validator; |
| this.extraExprs = extraExprs; |
| this.measureExprs = measureExprs; |
| this.groupExprs = groupExprs; |
| this.distinct = distinct; |
| this.scopes.push(scope); |
| } |
| |
| //~ Methods ---------------------------------------------------------------- |
| |
| boolean isGroupExpr(SqlNode e) { |
| for (SqlNode expr : Iterables.concat(extraExprs, measureExprs, groupExprs)) { |
| if (expr.equalsDeep(e, Litmus.IGNORE)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean isMeasureExp(SqlNode e) { |
| for (SqlNode expr : measureExprs) { |
| if (expr.equalsDeep(e, Litmus.IGNORE)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override public Void visit(SqlIdentifier id) { |
| if (id.isStar()) { |
| // Star may validly occur in "SELECT COUNT(*) OVER w" |
| return null; |
| } |
| |
| if (!validator.config().nakedMeasuresInAggregateQuery() |
| && isMeasureExp(id)) { |
| SqlNode originalExpr = validator.getOriginal(id); |
| throw validator.newValidationError(originalExpr, |
| RESOURCE.measureIllegal()); |
| } |
| |
| if (isGroupExpr(id)) { |
| return null; |
| } |
| |
| // Is it a call to a parentheses-free function? |
| final SqlCall call = validator.makeNullaryCall(id); |
| if (call != null) { |
| return call.accept(this); |
| } |
| |
| // Didn't find the identifier in the group-by list as is, now find |
| // it fully-qualified. |
| // TODO: It would be better if we always compared fully-qualified |
| // to fully-qualified. |
| final SqlQualified fqId = scopes.getFirst().fullyQualify(id); |
| if (isGroupExpr(fqId.identifier)) { |
| return null; |
| } |
| SqlNode originalExpr = validator.getOriginal(id); |
| final String exprString = originalExpr.toString(); |
| throw validator.newValidationError(originalExpr, |
| distinct |
| ? RESOURCE.notSelectDistinctExpr(exprString) |
| : RESOURCE.notGroupExpr(exprString)); |
| } |
| |
| @Override public Void visit(SqlCall call) { |
| final SqlValidatorScope scope = |
| requireNonNull(scopes.peek(), () -> "scope for " + call); |
| if (call.getOperator().isAggregator()) { |
| if (distinct) { |
| if (scope instanceof AggregatingSelectScope) { |
| final SqlSelect select = (SqlSelect) scope.getNode(); |
| SelectScope selectScope = |
| requireNonNull(validator.getRawSelectScope(select), |
| () -> "rawSelectScope for " + scope.getNode()); |
| List<SqlNode> selectList = |
| requireNonNull(selectScope.getExpandedSelectList(), |
| () -> "expandedSelectList for " + selectScope); |
| |
| // Check if this aggregation function is just an element in the select |
| for (SqlNode sqlNode : selectList) { |
| if (sqlNode.getKind() == SqlKind.AS) { |
| sqlNode = ((SqlCall) sqlNode).operand(0); |
| } |
| |
| if (validator.expand(sqlNode, scope) |
| .equalsDeep(call, Litmus.IGNORE)) { |
| return null; |
| } |
| } |
| } |
| |
| // Cannot use agg fun in ORDER BY clause if have SELECT DISTINCT. |
| SqlNode originalExpr = validator.getOriginal(call); |
| final String exprString = originalExpr.toString(); |
| throw validator.newValidationError(call, |
| RESOURCE.notSelectDistinctExpr(exprString)); |
| } |
| |
| // For example, 'sum(sal)' in 'SELECT sum(sal) FROM emp GROUP |
| // BY deptno' |
| return null; |
| } |
| switch (call.getKind()) { |
| case FILTER: |
| case WITHIN_GROUP: |
| case RESPECT_NULLS: |
| case IGNORE_NULLS: |
| case WITHIN_DISTINCT: |
| call.operand(0).accept(this); |
| return null; |
| default: |
| break; |
| } |
| // Visit the operand in window function |
| if (call.getKind() == SqlKind.OVER) { |
| for (SqlNode operand : call.<SqlCall>operand(0).getOperandList()) { |
| operand.accept(this); |
| } |
| // Check the OVER clause |
| final SqlNode over = call.operand(1); |
| if (over instanceof SqlCall) { |
| over.accept(this); |
| } else if (over instanceof SqlIdentifier) { |
| // Check the corresponding SqlWindow in WINDOW clause |
| final SqlWindow window = |
| scope.lookupWindow(((SqlIdentifier) over).getSimple()); |
| requireNonNull(window, () -> "window for " + call); |
| window.getPartitionList().accept(this); |
| window.getOrderList().accept(this); |
| } |
| } |
| if (isGroupExpr(call)) { |
| // This call matches an expression in the GROUP BY clause. |
| return null; |
| } |
| |
| final SqlCall groupCall = |
| SqlStdOperatorTable.convertAuxiliaryToGroupCall(call); |
| if (groupCall != null) { |
| if (isGroupExpr(groupCall)) { |
| // This call is an auxiliary function that matches a group call in the |
| // GROUP BY clause. |
| // |
| // For example TUMBLE_START is an auxiliary of the TUMBLE |
| // group function, and |
| // TUMBLE_START(rowtime, INTERVAL '1' HOUR) |
| // matches |
| // TUMBLE(rowtime, INTERVAL '1' HOUR') |
| return null; |
| } |
| throw validator.newValidationError(groupCall, |
| RESOURCE.auxiliaryWithoutMatchingGroupCall( |
| call.getOperator().getName(), groupCall.getOperator().getName())); |
| } |
| |
| if (call.isA(SqlKind.QUERY)) { |
| // Allow queries for now, even though they may contain |
| // references to forbidden columns. |
| return null; |
| } |
| |
| // Switch to new scope. |
| SqlValidatorScope newScope = scope.getOperandScope(call); |
| scopes.push(newScope); |
| |
| // Visit the operands (only expressions). |
| call.getOperator() |
| .acceptCall(this, call, true, ArgHandlerImpl.instance()); |
| |
| // Restore scope. |
| scopes.pop(); |
| return null; |
| } |
| |
| } |