com.hazelcast.org.apache.calcite.sql.validate.AggregatingSelectScope Maven / Gradle / Ivy
/*
* 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 com.hazelcast.org.apache.calcite.sql.validate;
import com.hazelcast.org.apache.calcite.linq4j.Linq4j;
import com.hazelcast.org.apache.calcite.linq4j.Ord;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.sql.SqlCall;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlSelect;
import com.hazelcast.org.apache.calcite.util.ImmutableBitSet;
import com.hazelcast.org.apache.calcite.util.Litmus;
import com.hazelcast.org.apache.calcite.util.Pair;
import com.hazelcast.com.google.common.base.Suppliers;
import com.hazelcast.com.google.common.collect.ImmutableList;
import com.hazelcast.com.google.common.collect.ImmutableMap;
import com.hazelcast.com.google.common.collect.ImmutableSet;
import com.hazelcast.com.google.common.collect.ImmutableSortedMultiset;
import com.hazelcast.org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import static com.hazelcast.org.apache.calcite.sql.SqlUtil.stripAs;
/**
* Scope for resolving identifiers within a SELECT statement that has a
* GROUP BY clause.
*
* The same set of identifiers are in scope, but it won't allow access to
* identifiers or expressions which are not group-expressions.
*/
public class AggregatingSelectScope
extends DelegatingScope implements AggregatingScope {
//~ Instance fields --------------------------------------------------------
private final SqlSelect select;
private final boolean distinct;
/** Use while resolving. */
private SqlValidatorUtil.@Nullable GroupAnalyzer groupAnalyzer;
@SuppressWarnings("methodref.receiver.bound.invalid")
public final Supplier resolved =
Suppliers.memoize(this::resolve)::get;
//~ Constructors -----------------------------------------------------------
/**
* Creates an AggregatingSelectScope.
*
* @param selectScope Parent scope
* @param select Enclosing SELECT node
* @param distinct Whether SELECT is DISTINCT
*/
AggregatingSelectScope(
SqlValidatorScope selectScope,
SqlSelect select,
boolean distinct) {
// The select scope is the parent in the sense that all columns which
// are available in the select scope are available. Whether they are
// valid as aggregation expressions... now that's a different matter.
super(selectScope);
this.select = select;
this.distinct = distinct;
}
//~ Methods ----------------------------------------------------------------
private Resolved resolve() {
assert groupAnalyzer == null : "resolve already in progress";
SqlValidatorUtil.GroupAnalyzer groupAnalyzer = new SqlValidatorUtil.GroupAnalyzer();
this.groupAnalyzer = groupAnalyzer;
try {
final ImmutableList.Builder> builder =
ImmutableList.builder();
boolean groupByDistinct = false;
if (select.getGroup() != null) {
SqlNodeList groupList = select.getGroup();
// if the DISTINCT keyword of GROUP BY is present it can be the only item
if (groupList.size() == 1 && groupList.get(0).getKind() == SqlKind.GROUP_BY_DISTINCT) {
groupList = new SqlNodeList(((SqlCall) groupList.get(0)).getOperandList(),
groupList.getParserPosition());
groupByDistinct = true;
}
for (SqlNode groupExpr : groupList) {
SqlValidatorUtil.analyzeGroupItem(this, groupAnalyzer, builder,
groupExpr);
}
}
final List flatGroupSets = new ArrayList<>();
for (List groupSet : Linq4j.product(builder.build())) {
flatGroupSets.add(ImmutableBitSet.union(groupSet));
}
// For GROUP BY (), we need a singleton grouping set.
if (flatGroupSets.isEmpty()) {
flatGroupSets.add(ImmutableBitSet.of());
}
if (groupByDistinct) {
ImmutableSet sets = ImmutableSet.copyOf(flatGroupSets);
flatGroupSets.clear();
flatGroupSets.addAll(sets);
}
return new Resolved(groupAnalyzer.extraExprs, groupAnalyzer.groupExprs,
flatGroupSets, groupAnalyzer.groupExprProjection);
} finally {
this.groupAnalyzer = null;
}
}
/**
* Returns the expressions that are in the GROUP BY clause (or the SELECT
* DISTINCT clause, if distinct) and that can therefore be referenced
* without being wrapped in aggregate functions.
*
* The expressions are fully-qualified, and any "*" in select clauses are
* expanded.
*
* @return list of grouping expressions
*/
private Pair, ImmutableList> getGroupExprs() {
if (distinct) {
// Cannot compute this in the constructor: select list has not been
// expanded yet.
assert select.isDistinct();
// Remove the AS operator so the expressions are consistent with
// OrderExpressionExpander.
ImmutableList.Builder groupExprs = ImmutableList.builder();
final SelectScope selectScope = (SelectScope) parent;
List expandedSelectList = Objects.requireNonNull(
selectScope.getExpandedSelectList(),
() -> "expandedSelectList for " + selectScope);
for (SqlNode selectItem : expandedSelectList) {
groupExprs.add(stripAs(selectItem));
}
return Pair.of(ImmutableList.of(), groupExprs.build());
} else if (select.getGroup() != null) {
SqlValidatorUtil.GroupAnalyzer groupAnalyzer = this.groupAnalyzer;
if (groupAnalyzer != null) {
// we are in the middle of resolving
return Pair.of(ImmutableList.of(),
ImmutableList.copyOf(groupAnalyzer.groupExprs));
} else {
final Resolved resolved = this.resolved.get();
return Pair.of(resolved.extraExprList, resolved.groupExprList);
}
} else {
return Pair.of(ImmutableList.of(), ImmutableList.of());
}
}
@Override public SqlNode getNode() {
return select;
}
@Override public RelDataType nullifyType(SqlNode node, RelDataType type) {
final Resolved r = this.resolved.get();
for (Ord groupExpr : Ord.zip(r.groupExprList)) {
if (groupExpr.e.equalsDeep(node, Litmus.IGNORE)) {
if (r.isNullable(groupExpr.i)) {
return validator.getTypeFactory().createTypeWithNullability(type,
true);
}
}
}
return type;
}
@Override public SqlValidatorScope getOperandScope(SqlCall call) {
if (call.getOperator().isAggregator()) {
// If we're the 'SUM' node in 'select a + sum(b + c) from t
// group by a', then we should validate our arguments in
// the non-aggregating scope, where 'b' and 'c' are valid
// column references.
return parent;
} else {
// Check whether expression is constant within the group.
//
// If not, throws. Example, 'empno' in
// SELECT empno FROM emp GROUP BY deptno
//
// If it perfectly matches an expression in the GROUP BY
// clause, we validate its arguments in the non-aggregating
// scope. Example, 'empno + 1' in
//
// SELECT empno + 1 FROM emp GROUP BY empno + 1
final boolean matches = checkAggregateExpr(call, false);
if (matches) {
return parent;
}
}
return super.getOperandScope(call);
}
@Override public boolean checkAggregateExpr(SqlNode expr, boolean deep) {
// Fully-qualify any identifiers in expr.
if (deep) {
expr = validator.expand(expr, this);
}
// Make sure expression is valid, throws if not.
Pair, ImmutableList> pair = getGroupExprs();
final AggChecker aggChecker =
new AggChecker(validator, this, pair.left, pair.right, distinct);
if (deep) {
expr.accept(aggChecker);
}
// Return whether expression exactly matches one of the group
// expressions.
return aggChecker.isGroupExpr(expr);
}
@Override public void validateExpr(SqlNode expr) {
checkAggregateExpr(expr, true);
}
/** Information about an aggregating scope that can only be determined
* after validation has occurred. Therefore it cannot be populated when
* the scope is created. */
@SuppressWarnings("UnstableApiUsage")
public static class Resolved {
public final ImmutableList extraExprList;
public final ImmutableList groupExprList;
public final ImmutableBitSet groupSet;
public final ImmutableSortedMultiset groupSets;
public final Map groupExprProjection;
Resolved(List extraExprList, List groupExprList,
Iterable groupSets,
Map groupExprProjection) {
this.extraExprList = ImmutableList.copyOf(extraExprList);
this.groupExprList = ImmutableList.copyOf(groupExprList);
this.groupSet = ImmutableBitSet.range(groupExprList.size());
this.groupSets = ImmutableSortedMultiset.copyOf(groupSets);
this.groupExprProjection = ImmutableMap.copyOf(groupExprProjection);
}
/** Returns whether a field should be nullable due to grouping sets. */
public boolean isNullable(int i) {
return i < groupExprList.size() && !ImmutableBitSet.allContain(groupSets, i);
}
/** Returns whether a given expression is equal to one of the grouping
* expressions. Determines whether it is valid as an operand to GROUPING. */
public boolean isGroupingExpr(SqlNode operand) {
return lookupGroupingExpr(operand) >= 0;
}
public int lookupGroupingExpr(SqlNode operand) {
for (Ord groupExpr : Ord.zip(groupExprList)) {
if (operand.equalsDeep(groupExpr.e, Litmus.IGNORE)) {
return groupExpr.i;
}
}
return -1;
}
}
}