You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by ja...@apache.org on 2020/02/07 02:28:30 UTC
[flink] 01/02: [FLINK-15935][table] Fix watermark can't work when
depending both on flink planner and blink planner in project
This is an automated email from the ASF dual-hosted git repository.
jark pushed a commit to branch release-1.10
in repository https://gitbox.apache.org/repos/asf/flink.git
commit cdf125f2059122fee44d6819596f454a82259204
Author: Jark Wu <ja...@apache.org>
AuthorDate: Thu Feb 6 16:37:08 2020 +0800
[FLINK-15935][table] Fix watermark can't work when depending both on flink planner and blink planner in project
The reason is that we didn't sync the fixing of org.apache.calcite.sql.validate.ParameterScope to flink-planner. When an application project depends flink-planner and blink-planner at the same time, the classloader may use the ParameterScope from Calcite instead of from blink planner, which leads to this exception.
This closes #11030
---
.../calcite/sql/validate/SqlValidatorImpl.java | 6451 ++++++++++++++++++++
.../validation/MatchRecognizeValidationTest.scala | 4 +-
.../calcite/sql/validate/ParameterScope.java | 72 +
.../apache/calcite/sql2rel/RelDecorrelator.java | 2857 ---------
4 files changed, 6524 insertions(+), 2860 deletions(-)
diff --git a/flink-table/flink-table-planner-blink/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/flink-table/flink-table-planner-blink/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
new file mode 100644
index 0000000..05615f7
--- /dev/null
+++ b/flink-table/flink-table-planner-blink/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -0,0 +1,6451 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.apache.calcite.config.NullCollation;
+import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.linq4j.function.Function2;
+import org.apache.calcite.linq4j.function.Functions;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.prepare.Prepare;
+import org.apache.calcite.rel.type.DynamicRecordType;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rel.type.RelRecordType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexPatternFieldRef;
+import org.apache.calcite.rex.RexVisitor;
+import org.apache.calcite.runtime.CalciteContextException;
+import org.apache.calcite.runtime.CalciteException;
+import org.apache.calcite.runtime.Feature;
+import org.apache.calcite.runtime.Resources;
+import org.apache.calcite.schema.ColumnStrategy;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.impl.ModifiableViewTable;
+import org.apache.calcite.sql.JoinConditionType;
+import org.apache.calcite.sql.JoinType;
+import org.apache.calcite.sql.SqlAccessEnum;
+import org.apache.calcite.sql.SqlAccessType;
+import org.apache.calcite.sql.SqlAggFunction;
+import org.apache.calcite.sql.SqlBasicCall;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCallBinding;
+import org.apache.calcite.sql.SqlDataTypeSpec;
+import org.apache.calcite.sql.SqlDelete;
+import org.apache.calcite.sql.SqlDynamicParam;
+import org.apache.calcite.sql.SqlExplain;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlInsert;
+import org.apache.calcite.sql.SqlIntervalLiteral;
+import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlJoin;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlMatchRecognize;
+import org.apache.calcite.sql.SqlMerge;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlOperatorTable;
+import org.apache.calcite.sql.SqlOrderBy;
+import org.apache.calcite.sql.SqlSampleSpec;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlSelectKeyword;
+import org.apache.calcite.sql.SqlSnapshot;
+import org.apache.calcite.sql.SqlSyntax;
+import org.apache.calcite.sql.SqlUnresolvedFunction;
+import org.apache.calcite.sql.SqlUpdate;
+import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql.SqlWindow;
+import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.SqlWithItem;
+import org.apache.calcite.sql.fun.SqlCase;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.AssignableOperandTypeChecker;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlOperandTypeInference;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.calcite.sql.util.SqlShuttle;
+import org.apache.calcite.sql.util.SqlVisitor;
+import org.apache.calcite.sql.validate.implicit.TypeCoercion;
+import org.apache.calcite.sql.validate.implicit.TypeCoercions;
+import org.apache.calcite.sql2rel.InitializerContext;
+import org.apache.calcite.util.BitString;
+import org.apache.calcite.util.Bug;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.ImmutableIntList;
+import org.apache.calcite.util.ImmutableNullableList;
+import org.apache.calcite.util.Litmus;
+import org.apache.calcite.util.Pair;
+import org.apache.calcite.util.Static;
+import org.apache.calcite.util.Util;
+import org.apache.calcite.util.trace.CalciteTrace;
+import org.slf4j.Logger;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.AbstractList;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import static org.apache.calcite.sql.SqlUtil.stripAs;
+import static org.apache.calcite.util.Static.RESOURCE;
+
+/*
+ * THIS FILE HAS BEEN COPIED FROM THE APACHE CALCITE PROJECT UNTIL CALCITE-2707 IS FIXED.
+ * (Added lines: 6016-6022)
+ */
+
+/**
+ * Default implementation of {@link SqlValidator}.
+ */
+public class SqlValidatorImpl implements SqlValidatorWithHints {
+ //~ Static fields/initializers ---------------------------------------------
+
+ public static final Logger TRACER = CalciteTrace.PARSER_LOGGER;
+
+ /**
+ * Alias generated for the source table when rewriting UPDATE to MERGE.
+ */
+ public static final String UPDATE_SRC_ALIAS = "SYS$SRC";
+
+ /**
+ * Alias generated for the target table when rewriting UPDATE to MERGE if no
+ * alias was specified by the user.
+ */
+ public static final String UPDATE_TGT_ALIAS = "SYS$TGT";
+
+ /**
+ * Alias prefix generated for source columns when rewriting UPDATE to MERGE.
+ */
+ public static final String UPDATE_ANON_PREFIX = "SYS$ANON";
+
+ //~ Instance fields --------------------------------------------------------
+
+ private final SqlOperatorTable opTab;
+ final SqlValidatorCatalogReader catalogReader;
+
+ /**
+ * Maps ParsePosition strings to the {@link SqlIdentifier} identifier
+ * objects at these positions
+ */
+ protected final Map<String, IdInfo> idPositions = new HashMap<>();
+
+ /**
+ * Maps {@link SqlNode query node} objects to the {@link SqlValidatorScope}
+ * scope created from them.
+ */
+ protected final Map<SqlNode, SqlValidatorScope> scopes =
+ new IdentityHashMap<>();
+
+ /**
+ * Maps a {@link SqlSelect} node to the scope used by its WHERE and HAVING
+ * clauses.
+ */
+ private final Map<SqlSelect, SqlValidatorScope> whereScopes =
+ new IdentityHashMap<>();
+
+ /**
+ * Maps a {@link SqlSelect} node to the scope used by its GROUP BY clause.
+ */
+ private final Map<SqlSelect, SqlValidatorScope> groupByScopes =
+ new IdentityHashMap<>();
+
+ /**
+ * Maps a {@link SqlSelect} node to the scope used by its SELECT and HAVING
+ * clauses.
+ */
+ private final Map<SqlSelect, SqlValidatorScope> selectScopes =
+ new IdentityHashMap<>();
+
+ /**
+ * Maps a {@link SqlSelect} node to the scope used by its ORDER BY clause.
+ */
+ private final Map<SqlSelect, SqlValidatorScope> orderScopes =
+ new IdentityHashMap<>();
+
+ /**
+ * Maps a {@link SqlSelect} node that is the argument to a CURSOR
+ * constructor to the scope of the result of that select node
+ */
+ private final Map<SqlSelect, SqlValidatorScope> cursorScopes =
+ new IdentityHashMap<>();
+
+ /**
+ * The name-resolution scope of a LATERAL TABLE clause.
+ */
+ private TableScope tableScope = null;
+
+ /**
+ * Maps a {@link SqlNode node} to the
+ * {@link SqlValidatorNamespace namespace} which describes what columns they
+ * contain.
+ */
+ protected final Map<SqlNode, SqlValidatorNamespace> namespaces =
+ new IdentityHashMap<>();
+
+ /**
+ * Set of select expressions used as cursor definitions. In standard SQL,
+ * only the top-level SELECT is a cursor; Calcite extends this with
+ * cursors as inputs to table functions.
+ */
+ private final Set<SqlNode> cursorSet = Sets.newIdentityHashSet();
+
+ /**
+ * Stack of objects that maintain information about function calls. A stack
+ * is needed to handle nested function calls. The function call currently
+ * being validated is at the top of the stack.
+ */
+ protected final Deque<FunctionParamInfo> functionCallStack =
+ new ArrayDeque<>();
+
+ private int nextGeneratedId;
+ protected final RelDataTypeFactory typeFactory;
+
+ /** The type of dynamic parameters until a type is imposed on them. */
+ protected final RelDataType unknownType;
+ private final RelDataType booleanType;
+
+ /**
+ * Map of derived RelDataType for each node. This is an IdentityHashMap
+ * since in some cases (such as null literals) we need to discriminate by
+ * instance.
+ */
+ private final Map<SqlNode, RelDataType> nodeToTypeMap =
+ new IdentityHashMap<>();
+ private final AggFinder aggFinder;
+ private final AggFinder aggOrOverFinder;
+ private final AggFinder aggOrOverOrGroupFinder;
+ private final AggFinder groupFinder;
+ private final AggFinder overFinder;
+ private final SqlConformance conformance;
+ private final Map<SqlNode, SqlNode> originalExprs = new HashMap<>();
+
+ private SqlNode top;
+
+ // REVIEW jvs 30-June-2006: subclasses may override shouldExpandIdentifiers
+ // in a way that ignores this; we should probably get rid of the protected
+ // method and always use this variable (or better, move preferences like
+ // this to a separate "parameter" class)
+ protected boolean expandIdentifiers;
+
+ protected boolean expandColumnReferences;
+
+ private boolean rewriteCalls;
+
+ private NullCollation nullCollation = NullCollation.HIGH;
+
+ // TODO jvs 11-Dec-2008: make this local to performUnconditionalRewrites
+ // if it's OK to expand the signature of that method.
+ private boolean validatingSqlMerge;
+
+ private boolean inWindow; // Allow nested aggregates
+
+ private final ValidationErrorFunction validationErrorFunction =
+ new ValidationErrorFunction();
+
+ // TypeCoercion instance used for implicit type coercion.
+ private TypeCoercion typeCoercion;
+
+ // Flag saying if we enable the implicit type coercion.
+ private boolean enableTypeCoercion;
+
+ //~ Constructors -----------------------------------------------------------
+
+ /**
+ * Creates a validator.
+ *
+ * @param opTab Operator table
+ * @param catalogReader Catalog reader
+ * @param typeFactory Type factory
+ * @param conformance Compatibility mode
+ */
+ protected SqlValidatorImpl(
+ SqlOperatorTable opTab,
+ SqlValidatorCatalogReader catalogReader,
+ RelDataTypeFactory typeFactory,
+ SqlConformance conformance) {
+ this.opTab = Objects.requireNonNull(opTab);
+ this.catalogReader = Objects.requireNonNull(catalogReader);
+ this.typeFactory = Objects.requireNonNull(typeFactory);
+ this.conformance = Objects.requireNonNull(conformance);
+
+ unknownType = typeFactory.createUnknownType();
+ booleanType = typeFactory.createSqlType(SqlTypeName.BOOLEAN);
+
+ rewriteCalls = true;
+ expandColumnReferences = true;
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ aggFinder = new AggFinder(opTab, false, true, false, null, nameMatcher);
+ aggOrOverFinder = new AggFinder(opTab, true, true, false, null, nameMatcher);
+ overFinder = new AggFinder(opTab, true, false, false, aggOrOverFinder, nameMatcher);
+ groupFinder = new AggFinder(opTab, false, false, true, null, nameMatcher);
+ aggOrOverOrGroupFinder = new AggFinder(opTab, true, true, true, null, nameMatcher);
+ this.enableTypeCoercion = catalogReader.getConfig() == null
+ || catalogReader.getConfig().typeCoercion();
+ this.typeCoercion = TypeCoercions.getTypeCoercion(this, conformance);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public SqlConformance getConformance() {
+ return conformance;
+ }
+
+ public SqlValidatorCatalogReader getCatalogReader() {
+ return catalogReader;
+ }
+
+ public SqlOperatorTable getOperatorTable() {
+ return opTab;
+ }
+
+ public RelDataTypeFactory getTypeFactory() {
+ return typeFactory;
+ }
+
+ public RelDataType getUnknownType() {
+ return unknownType;
+ }
+
+ public SqlNodeList expandStar(
+ SqlNodeList selectList,
+ SqlSelect select,
+ boolean includeSystemVars) {
+ final List<SqlNode> list = new ArrayList<>();
+ final List<Map.Entry<String, RelDataType>> types = new ArrayList<>();
+ for (int i = 0; i < selectList.size(); i++) {
+ final SqlNode selectItem = selectList.get(i);
+ final RelDataType originalType = getValidatedNodeTypeIfKnown(selectItem);
+ expandSelectItem(
+ selectItem,
+ select,
+ Util.first(originalType, unknownType),
+ list,
+ catalogReader.nameMatcher().createSet(),
+ types,
+ includeSystemVars);
+ }
+ getRawSelectScope(select).setExpandedSelectList(list);
+ return new SqlNodeList(list, SqlParserPos.ZERO);
+ }
+
+ // implement SqlValidator
+ public void declareCursor(SqlSelect select, SqlValidatorScope parentScope) {
+ cursorSet.add(select);
+
+ // add the cursor to a map that maps the cursor to its select based on
+ // the position of the cursor relative to other cursors in that call
+ FunctionParamInfo funcParamInfo = functionCallStack.peek();
+ Map<Integer, SqlSelect> cursorMap = funcParamInfo.cursorPosToSelectMap;
+ int numCursors = cursorMap.size();
+ cursorMap.put(numCursors, select);
+
+ // create a namespace associated with the result of the select
+ // that is the argument to the cursor constructor; register it
+ // with a scope corresponding to the cursor
+ SelectScope cursorScope = new SelectScope(parentScope, null, select);
+ cursorScopes.put(select, cursorScope);
+ final SelectNamespace selectNs = createSelectNamespace(select, select);
+ String alias = deriveAlias(select, nextGeneratedId++);
+ registerNamespace(cursorScope, alias, selectNs, false);
+ }
+
+ // implement SqlValidator
+ public void pushFunctionCall() {
+ FunctionParamInfo funcInfo = new FunctionParamInfo();
+ functionCallStack.push(funcInfo);
+ }
+
+ // implement SqlValidator
+ public void popFunctionCall() {
+ functionCallStack.pop();
+ }
+
+ // implement SqlValidator
+ public String getParentCursor(String columnListParamName) {
+ FunctionParamInfo funcParamInfo = functionCallStack.peek();
+ Map<String, String> parentCursorMap =
+ funcParamInfo.columnListParamToParentCursorMap;
+ return parentCursorMap.get(columnListParamName);
+ }
+
+ /**
+ * If <code>selectItem</code> is "*" or "TABLE.*", expands it and returns
+ * true; otherwise writes the unexpanded item.
+ *
+ * @param selectItem Select-list item
+ * @param select Containing select clause
+ * @param selectItems List that expanded items are written to
+ * @param aliases Set of aliases
+ * @param fields List of field names and types, in alias order
+ * @param includeSystemVars If true include system vars in lists
+ * @return Whether the node was expanded
+ */
+ private boolean expandSelectItem(
+ final SqlNode selectItem,
+ SqlSelect select,
+ RelDataType targetType,
+ List<SqlNode> selectItems,
+ Set<String> aliases,
+ List<Map.Entry<String, RelDataType>> fields,
+ final boolean includeSystemVars) {
+ final SelectScope scope = (SelectScope) getWhereScope(select);
+ if (expandStar(selectItems, aliases, fields, includeSystemVars, scope,
+ selectItem)) {
+ return true;
+ }
+
+ // Expand the select item: fully-qualify columns, and convert
+ // parentheses-free functions such as LOCALTIME into explicit function
+ // calls.
+ SqlNode expanded = expand(selectItem, scope);
+ final String alias =
+ deriveAlias(
+ selectItem,
+ aliases.size());
+
+ // If expansion has altered the natural alias, supply an explicit 'AS'.
+ final SqlValidatorScope selectScope = getSelectScope(select);
+ if (expanded != selectItem) {
+ String newAlias =
+ deriveAlias(
+ expanded,
+ aliases.size());
+ if (!newAlias.equals(alias)) {
+ expanded =
+ SqlStdOperatorTable.AS.createCall(
+ selectItem.getParserPosition(),
+ expanded,
+ new SqlIdentifier(alias, SqlParserPos.ZERO));
+ deriveTypeImpl(selectScope, expanded);
+ }
+ }
+
+ selectItems.add(expanded);
+ aliases.add(alias);
+
+ if (expanded != null) {
+ inferUnknownTypes(targetType, scope, expanded);
+ }
+ final RelDataType type = deriveType(selectScope, expanded);
+ setValidatedNodeType(expanded, type);
+ fields.add(Pair.of(alias, type));
+ return false;
+ }
+
+ private boolean expandStar(List<SqlNode> selectItems, Set<String> aliases,
+ List<Map.Entry<String, RelDataType>> fields, boolean includeSystemVars,
+ SelectScope scope, SqlNode node) {
+ if (!(node instanceof SqlIdentifier)) {
+ return false;
+ }
+ final SqlIdentifier identifier = (SqlIdentifier) node;
+ if (!identifier.isStar()) {
+ return false;
+ }
+ final SqlParserPos startPosition = identifier.getParserPosition();
+ switch (identifier.names.size()) {
+ case 1:
+ boolean hasDynamicStruct = false;
+ for (ScopeChild child : scope.children) {
+ final int before = fields.size();
+ if (child.namespace.getRowType().isDynamicStruct()) {
+ hasDynamicStruct = true;
+ // don't expand star if the underneath table is dynamic.
+ // Treat this star as a special field in validation/conversion and
+ // wait until execution time to expand this star.
+ final SqlNode exp =
+ new SqlIdentifier(
+ ImmutableList.of(child.name,
+ DynamicRecordType.DYNAMIC_STAR_PREFIX),
+ startPosition);
+ addToSelectList(
+ selectItems,
+ aliases,
+ fields,
+ exp,
+ scope,
+ includeSystemVars);
+ } else {
+ final SqlNode from = child.namespace.getNode();
+ final SqlValidatorNamespace fromNs = getNamespace(from, scope);
+ assert fromNs != null;
+ final RelDataType rowType = fromNs.getRowType();
+ for (RelDataTypeField field : rowType.getFieldList()) {
+ String columnName = field.getName();
+
+ // TODO: do real implicit collation here
+ final SqlIdentifier exp =
+ new SqlIdentifier(
+ ImmutableList.of(child.name, columnName),
+ startPosition);
+ // Don't add expanded rolled up columns
+ if (!isRolledUpColumn(exp, scope)) {
+ addOrExpandField(
+ selectItems,
+ aliases,
+ fields,
+ includeSystemVars,
+ scope,
+ exp,
+ field);
+ }
+ }
+ }
+ if (child.nullable) {
+ for (int i = before; i < fields.size(); i++) {
+ final Map.Entry<String, RelDataType> entry = fields.get(i);
+ final RelDataType type = entry.getValue();
+ if (!type.isNullable()) {
+ fields.set(i,
+ Pair.of(entry.getKey(),
+ typeFactory.createTypeWithNullability(type, true)));
+ }
+ }
+ }
+ }
+ // If NATURAL JOIN or USING is present, move key fields to the front of
+ // the list, per standard SQL. Disabled if there are dynamic fields.
+ if (!hasDynamicStruct || Bug.CALCITE_2400_FIXED) {
+ new Permute(scope.getNode().getFrom(), 0).permute(selectItems, fields);
+ }
+ return true;
+
+ default:
+ final SqlIdentifier prefixId = identifier.skipLast(1);
+ final SqlValidatorScope.ResolvedImpl resolved =
+ new SqlValidatorScope.ResolvedImpl();
+ final SqlNameMatcher nameMatcher =
+ scope.validator.catalogReader.nameMatcher();
+ scope.resolve(prefixId.names, nameMatcher, true, resolved);
+ if (resolved.count() == 0) {
+ // e.g. "select s.t.* from e"
+ // or "select r.* from e"
+ throw newValidationError(prefixId,
+ RESOURCE.unknownIdentifier(prefixId.toString()));
+ }
+ final RelDataType rowType = resolved.only().rowType();
+ if (rowType.isDynamicStruct()) {
+ // don't expand star if the underneath table is dynamic.
+ addToSelectList(
+ selectItems,
+ aliases,
+ fields,
+ prefixId.plus(DynamicRecordType.DYNAMIC_STAR_PREFIX, startPosition),
+ scope,
+ includeSystemVars);
+ } else if (rowType.isStruct()) {
+ for (RelDataTypeField field : rowType.getFieldList()) {
+ String columnName = field.getName();
+
+ // TODO: do real implicit collation here
+ addOrExpandField(
+ selectItems,
+ aliases,
+ fields,
+ includeSystemVars,
+ scope,
+ prefixId.plus(columnName, startPosition),
+ field);
+ }
+ } else {
+ throw newValidationError(prefixId, RESOURCE.starRequiresRecordType());
+ }
+ return true;
+ }
+ }
+
+ private SqlNode maybeCast(SqlNode node, RelDataType currentType,
+ RelDataType desiredType) {
+ return currentType.equals(desiredType)
+ || (currentType.isNullable() != desiredType.isNullable()
+ && typeFactory.createTypeWithNullability(currentType,
+ desiredType.isNullable()).equals(desiredType))
+ ? node
+ : SqlStdOperatorTable.CAST.createCall(SqlParserPos.ZERO,
+ node, SqlTypeUtil.convertTypeToSpec(desiredType));
+ }
+
+ private boolean addOrExpandField(List<SqlNode> selectItems, Set<String> aliases,
+ List<Map.Entry<String, RelDataType>> fields, boolean includeSystemVars,
+ SelectScope scope, SqlIdentifier id, RelDataTypeField field) {
+ switch (field.getType().getStructKind()) {
+ case PEEK_FIELDS:
+ case PEEK_FIELDS_DEFAULT:
+ final SqlNode starExp = id.plusStar();
+ expandStar(
+ selectItems,
+ aliases,
+ fields,
+ includeSystemVars,
+ scope,
+ starExp);
+ return true;
+
+ default:
+ addToSelectList(
+ selectItems,
+ aliases,
+ fields,
+ id,
+ scope,
+ includeSystemVars);
+ }
+
+ return false;
+ }
+
+ public SqlNode validate(SqlNode topNode) {
+ SqlValidatorScope scope = new EmptyScope(this);
+ scope = new CatalogScope(scope, ImmutableList.of("CATALOG"));
+ final SqlNode topNode2 = validateScopedExpression(topNode, scope);
+ final RelDataType type = getValidatedNodeType(topNode2);
+ Util.discard(type);
+ return topNode2;
+ }
+
+ public List<SqlMoniker> lookupHints(SqlNode topNode, SqlParserPos pos) {
+ SqlValidatorScope scope = new EmptyScope(this);
+ SqlNode outermostNode = performUnconditionalRewrites(topNode, false);
+ cursorSet.add(outermostNode);
+ if (outermostNode.isA(SqlKind.TOP_LEVEL)) {
+ registerQuery(
+ scope,
+ null,
+ outermostNode,
+ outermostNode,
+ null,
+ false);
+ }
+ final SqlValidatorNamespace ns = getNamespace(outermostNode);
+ if (ns == null) {
+ throw new AssertionError("Not a query: " + outermostNode);
+ }
+ Collection<SqlMoniker> hintList = Sets.newTreeSet(SqlMoniker.COMPARATOR);
+ lookupSelectHints(ns, pos, hintList);
+ return ImmutableList.copyOf(hintList);
+ }
+
+ public SqlMoniker lookupQualifiedName(SqlNode topNode, SqlParserPos pos) {
+ final String posString = pos.toString();
+ IdInfo info = idPositions.get(posString);
+ if (info != null) {
+ final SqlQualified qualified = info.scope.fullyQualify(info.id);
+ return new SqlIdentifierMoniker(qualified.identifier);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Looks up completion hints for a syntactically correct select SQL that has
+ * been parsed into an expression tree.
+ *
+ * @param select the Select node of the parsed expression tree
+ * @param pos indicates the position in the sql statement we want to get
+ * completion hints for
+ * @param hintList list of {@link SqlMoniker} (sql identifiers) that can
+ * fill in at the indicated position
+ */
+ void lookupSelectHints(
+ SqlSelect select,
+ SqlParserPos pos,
+ Collection<SqlMoniker> hintList) {
+ IdInfo info = idPositions.get(pos.toString());
+ if ((info == null) || (info.scope == null)) {
+ SqlNode fromNode = select.getFrom();
+ final SqlValidatorScope fromScope = getFromScope(select);
+ lookupFromHints(fromNode, fromScope, pos, hintList);
+ } else {
+ lookupNameCompletionHints(info.scope, info.id.names,
+ info.id.getParserPosition(), hintList);
+ }
+ }
+
+ private void lookupSelectHints(
+ SqlValidatorNamespace ns,
+ SqlParserPos pos,
+ Collection<SqlMoniker> hintList) {
+ final SqlNode node = ns.getNode();
+ if (node instanceof SqlSelect) {
+ lookupSelectHints((SqlSelect) node, pos, hintList);
+ }
+ }
+
+ private void lookupFromHints(
+ SqlNode node,
+ SqlValidatorScope scope,
+ SqlParserPos pos,
+ Collection<SqlMoniker> hintList) {
+ if (node == null) {
+ // This can happen in cases like "select * _suggest_", so from clause is absent
+ return;
+ }
+ final SqlValidatorNamespace ns = getNamespace(node);
+ if (ns.isWrapperFor(IdentifierNamespace.class)) {
+ IdentifierNamespace idNs = ns.unwrap(IdentifierNamespace.class);
+ final SqlIdentifier id = idNs.getId();
+ for (int i = 0; i < id.names.size(); i++) {
+ if (pos.toString().equals(
+ id.getComponent(i).getParserPosition().toString())) {
+ final List<SqlMoniker> objNames = new ArrayList<>();
+ SqlValidatorUtil.getSchemaObjectMonikers(
+ getCatalogReader(),
+ id.names.subList(0, i + 1),
+ objNames);
+ for (SqlMoniker objName : objNames) {
+ if (objName.getType() != SqlMonikerType.FUNCTION) {
+ hintList.add(objName);
+ }
+ }
+ return;
+ }
+ }
+ }
+ switch (node.getKind()) {
+ case JOIN:
+ lookupJoinHints((SqlJoin) node, scope, pos, hintList);
+ break;
+ default:
+ lookupSelectHints(ns, pos, hintList);
+ break;
+ }
+ }
+
+ private void lookupJoinHints(
+ SqlJoin join,
+ SqlValidatorScope scope,
+ SqlParserPos pos,
+ Collection<SqlMoniker> hintList) {
+ SqlNode left = join.getLeft();
+ SqlNode right = join.getRight();
+ SqlNode condition = join.getCondition();
+ lookupFromHints(left, scope, pos, hintList);
+ if (hintList.size() > 0) {
+ return;
+ }
+ lookupFromHints(right, scope, pos, hintList);
+ if (hintList.size() > 0) {
+ return;
+ }
+ final JoinConditionType conditionType = join.getConditionType();
+ final SqlValidatorScope joinScope = scopes.get(join);
+ switch (conditionType) {
+ case ON:
+ condition.findValidOptions(this, joinScope, pos, hintList);
+ return;
+ default:
+
+ // No suggestions.
+ // Not supporting hints for other types such as 'Using' yet.
+ return;
+ }
+ }
+
+ /**
+ * Populates a list of all the valid alternatives for an identifier.
+ *
+ * @param scope Validation scope
+ * @param names Components of the identifier
+ * @param pos position
+ * @param hintList a list of valid options
+ */
+ public final void lookupNameCompletionHints(
+ SqlValidatorScope scope,
+ List<String> names,
+ SqlParserPos pos,
+ Collection<SqlMoniker> hintList) {
+ // Remove the last part of name - it is a dummy
+ List<String> subNames = Util.skipLast(names);
+
+ if (subNames.size() > 0) {
+ // If there's a prefix, resolve it to a namespace.
+ SqlValidatorNamespace ns = null;
+ for (String name : subNames) {
+ if (ns == null) {
+ final SqlValidatorScope.ResolvedImpl resolved =
+ new SqlValidatorScope.ResolvedImpl();
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ scope.resolve(ImmutableList.of(name), nameMatcher, false, resolved);
+ if (resolved.count() == 1) {
+ ns = resolved.only().namespace;
+ }
+ } else {
+ ns = ns.lookupChild(name);
+ }
+ if (ns == null) {
+ break;
+ }
+ }
+ if (ns != null) {
+ RelDataType rowType = ns.getRowType();
+ if (rowType.isStruct()) {
+ for (RelDataTypeField field : rowType.getFieldList()) {
+ hintList.add(
+ new SqlMonikerImpl(
+ field.getName(),
+ SqlMonikerType.COLUMN));
+ }
+ }
+ }
+
+ // builtin function names are valid completion hints when the
+ // identifier has only 1 name part
+ findAllValidFunctionNames(names, this, hintList, pos);
+ } else {
+ // No prefix; use the children of the current scope (that is,
+ // the aliases in the FROM clause)
+ scope.findAliases(hintList);
+
+ // If there's only one alias, add all child columns
+ SelectScope selectScope =
+ SqlValidatorUtil.getEnclosingSelectScope(scope);
+ if ((selectScope != null)
+ && (selectScope.getChildren().size() == 1)) {
+ RelDataType rowType =
+ selectScope.getChildren().get(0).getRowType();
+ for (RelDataTypeField field : rowType.getFieldList()) {
+ hintList.add(
+ new SqlMonikerImpl(
+ field.getName(),
+ SqlMonikerType.COLUMN));
+ }
+ }
+ }
+
+ findAllValidUdfNames(names, this, hintList);
+ }
+
+ private static void findAllValidUdfNames(
+ List<String> names,
+ SqlValidator validator,
+ Collection<SqlMoniker> result) {
+ final List<SqlMoniker> objNames = new ArrayList<>();
+ SqlValidatorUtil.getSchemaObjectMonikers(
+ validator.getCatalogReader(),
+ names,
+ objNames);
+ for (SqlMoniker objName : objNames) {
+ if (objName.getType() == SqlMonikerType.FUNCTION) {
+ result.add(objName);
+ }
+ }
+ }
+
+ private static void findAllValidFunctionNames(
+ List<String> names,
+ SqlValidator validator,
+ Collection<SqlMoniker> result,
+ SqlParserPos pos) {
+ // a function name can only be 1 part
+ if (names.size() > 1) {
+ return;
+ }
+ for (SqlOperator op : validator.getOperatorTable().getOperatorList()) {
+ SqlIdentifier curOpId =
+ new SqlIdentifier(
+ op.getName(),
+ pos);
+
+ final SqlCall call = validator.makeNullaryCall(curOpId);
+ if (call != null) {
+ result.add(
+ new SqlMonikerImpl(
+ op.getName(),
+ SqlMonikerType.FUNCTION));
+ } else {
+ if ((op.getSyntax() == SqlSyntax.FUNCTION)
+ || (op.getSyntax() == SqlSyntax.PREFIX)) {
+ if (op.getOperandTypeChecker() != null) {
+ String sig = op.getAllowedSignatures();
+ sig = sig.replaceAll("'", "");
+ result.add(
+ new SqlMonikerImpl(
+ sig,
+ SqlMonikerType.FUNCTION));
+ continue;
+ }
+ result.add(
+ new SqlMonikerImpl(
+ op.getName(),
+ SqlMonikerType.FUNCTION));
+ }
+ }
+ }
+ }
+
+ public SqlNode validateParameterizedExpression(
+ SqlNode topNode,
+ final Map<String, RelDataType> nameToTypeMap) {
+ SqlValidatorScope scope = new ParameterScope(this, nameToTypeMap);
+ return validateScopedExpression(topNode, scope);
+ }
+
+ private SqlNode validateScopedExpression(
+ SqlNode topNode,
+ SqlValidatorScope scope) {
+ SqlNode outermostNode = performUnconditionalRewrites(topNode, false);
+ cursorSet.add(outermostNode);
+ top = outermostNode;
+ TRACER.trace("After unconditional rewrite: {}", outermostNode);
+ if (outermostNode.isA(SqlKind.TOP_LEVEL)) {
+ registerQuery(scope, null, outermostNode, outermostNode, null, false);
+ }
+ outermostNode.validate(this, scope);
+ if (!outermostNode.isA(SqlKind.TOP_LEVEL)) {
+ // force type derivation so that we can provide it to the
+ // caller later without needing the scope
+ deriveType(scope, outermostNode);
+ }
+ TRACER.trace("After validation: {}", outermostNode);
+ return outermostNode;
+ }
+
+ public void validateQuery(SqlNode node, SqlValidatorScope scope,
+ RelDataType targetRowType) {
+ final SqlValidatorNamespace ns = getNamespace(node, scope);
+ if (node.getKind() == SqlKind.TABLESAMPLE) {
+ List<SqlNode> operands = ((SqlCall) node).getOperandList();
+ SqlSampleSpec sampleSpec = SqlLiteral.sampleValue(operands.get(1));
+ if (sampleSpec instanceof SqlSampleSpec.SqlTableSampleSpec) {
+ validateFeature(RESOURCE.sQLFeature_T613(), node.getParserPosition());
+ } else if (sampleSpec
+ instanceof SqlSampleSpec.SqlSubstitutionSampleSpec) {
+ validateFeature(RESOURCE.sQLFeatureExt_T613_Substitution(),
+ node.getParserPosition());
+ }
+ }
+
+ validateNamespace(ns, targetRowType);
+ switch (node.getKind()) {
+ case EXTEND:
+ // Until we have a dedicated namespace for EXTEND
+ deriveType(scope, node);
+ }
+ if (node == top) {
+ validateModality(node);
+ }
+ validateAccess(
+ node,
+ ns.getTable(),
+ SqlAccessEnum.SELECT);
+
+ if (node.getKind() == SqlKind.SNAPSHOT) {
+ SqlSnapshot snapshot = (SqlSnapshot) node;
+ SqlNode period = snapshot.getPeriod();
+ RelDataType dataType = deriveType(scope, period);
+ if (dataType.getSqlTypeName() != SqlTypeName.TIMESTAMP) {
+ throw newValidationError(period,
+ Static.RESOURCE.illegalExpressionForTemporal(dataType.getSqlTypeName().getName()));
+ }
+ if (!ns.getTable().isTemporal()) {
+ List<String> qualifiedName = ns.getTable().getQualifiedName();
+ String tableName = qualifiedName.get(qualifiedName.size() - 1);
+ throw newValidationError(snapshot.getTableRef(),
+ Static.RESOURCE.notTemporalTable(tableName));
+ }
+ }
+ }
+
+ /**
+ * Validates a namespace.
+ *
+ * @param namespace Namespace
+ * @param targetRowType Desired row type, must not be null, may be the data
+ * type 'unknown'.
+ */
+ protected void validateNamespace(final SqlValidatorNamespace namespace,
+ RelDataType targetRowType) {
+ namespace.validate(targetRowType);
+ if (namespace.getNode() != null) {
+ setValidatedNodeType(namespace.getNode(), namespace.getType());
+ }
+ }
+
+ @VisibleForTesting
+ public SqlValidatorScope getEmptyScope() {
+ return new EmptyScope(this);
+ }
+
+ public SqlValidatorScope getCursorScope(SqlSelect select) {
+ return cursorScopes.get(select);
+ }
+
+ public SqlValidatorScope getWhereScope(SqlSelect select) {
+ return whereScopes.get(select);
+ }
+
+ public SqlValidatorScope getSelectScope(SqlSelect select) {
+ return selectScopes.get(select);
+ }
+
+ public SelectScope getRawSelectScope(SqlSelect select) {
+ SqlValidatorScope scope = getSelectScope(select);
+ if (scope instanceof AggregatingSelectScope) {
+ scope = ((AggregatingSelectScope) scope).getParent();
+ }
+ return (SelectScope) scope;
+ }
+
+ public SqlValidatorScope getHavingScope(SqlSelect select) {
+ // Yes, it's the same as getSelectScope
+ return selectScopes.get(select);
+ }
+
+ public SqlValidatorScope getGroupScope(SqlSelect select) {
+ // Yes, it's the same as getWhereScope
+ return groupByScopes.get(select);
+ }
+
+ public SqlValidatorScope getFromScope(SqlSelect select) {
+ return scopes.get(select);
+ }
+
+ public SqlValidatorScope getOrderScope(SqlSelect select) {
+ return orderScopes.get(select);
+ }
+
+ public SqlValidatorScope getMatchRecognizeScope(SqlMatchRecognize node) {
+ return scopes.get(node);
+ }
+
+ public SqlValidatorScope getJoinScope(SqlNode node) {
+ return scopes.get(stripAs(node));
+ }
+
+ public SqlValidatorScope getOverScope(SqlNode node) {
+ return scopes.get(node);
+ }
+
+ private SqlValidatorNamespace getNamespace(SqlNode node,
+ SqlValidatorScope scope) {
+ if (node instanceof SqlIdentifier && scope instanceof DelegatingScope) {
+ final SqlIdentifier id = (SqlIdentifier) node;
+ final DelegatingScope idScope = (DelegatingScope) ((DelegatingScope) scope).getParent();
+ return getNamespace(id, idScope);
+ } else if (node instanceof SqlCall) {
+ // Handle extended identifiers.
+ final SqlCall call = (SqlCall) node;
+ switch (call.getOperator().getKind()) {
+ case EXTEND:
+ final SqlIdentifier id = (SqlIdentifier) call.getOperandList().get(0);
+ final DelegatingScope idScope = (DelegatingScope) scope;
+ return getNamespace(id, idScope);
+ case AS:
+ final SqlNode nested = call.getOperandList().get(0);
+ switch (nested.getKind()) {
+ case EXTEND:
+ return getNamespace(nested, scope);
+ }
+ break;
+ }
+ }
+ return getNamespace(node);
+ }
+
+ private SqlValidatorNamespace getNamespace(SqlIdentifier id, DelegatingScope scope) {
+ if (id.isSimple()) {
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ final SqlValidatorScope.ResolvedImpl resolved =
+ new SqlValidatorScope.ResolvedImpl();
+ scope.resolve(id.names, nameMatcher, false, resolved);
+ if (resolved.count() == 1) {
+ return resolved.only().namespace;
+ }
+ }
+ return getNamespace(id);
+ }
+
+ public SqlValidatorNamespace getNamespace(SqlNode node) {
+ switch (node.getKind()) {
+ case AS:
+
+ // AS has a namespace if it has a column list 'AS t (c1, c2, ...)'
+ final SqlValidatorNamespace ns = namespaces.get(node);
+ if (ns != null) {
+ return ns;
+ }
+ // fall through
+ case SNAPSHOT:
+ case OVER:
+ case COLLECTION_TABLE:
+ case ORDER_BY:
+ case TABLESAMPLE:
+ return getNamespace(((SqlCall) node).operand(0));
+ default:
+ return namespaces.get(node);
+ }
+ }
+
+ private void handleOffsetFetch(SqlNode offset, SqlNode fetch) {
+ if (offset instanceof SqlDynamicParam) {
+ setValidatedNodeType(offset,
+ typeFactory.createSqlType(SqlTypeName.INTEGER));
+ }
+ if (fetch instanceof SqlDynamicParam) {
+ setValidatedNodeType(fetch,
+ typeFactory.createSqlType(SqlTypeName.INTEGER));
+ }
+ }
+
+ /**
+ * Performs expression rewrites which are always used unconditionally. These
+ * rewrites massage the expression tree into a standard form so that the
+ * rest of the validation logic can be simpler.
+ *
+ * @param node expression to be rewritten
+ * @param underFrom whether node appears directly under a FROM clause
+ * @return rewritten expression
+ */
+ protected SqlNode performUnconditionalRewrites(
+ SqlNode node,
+ boolean underFrom) {
+ if (node == null) {
+ return node;
+ }
+
+ SqlNode newOperand;
+
+ // first transform operands and invoke generic call rewrite
+ if (node instanceof SqlCall) {
+ if (node instanceof SqlMerge) {
+ validatingSqlMerge = true;
+ }
+ SqlCall call = (SqlCall) node;
+ final SqlKind kind = call.getKind();
+ final List<SqlNode> operands = call.getOperandList();
+ for (int i = 0; i < operands.size(); i++) {
+ SqlNode operand = operands.get(i);
+ boolean childUnderFrom;
+ if (kind == SqlKind.SELECT) {
+ childUnderFrom = i == SqlSelect.FROM_OPERAND;
+ } else if (kind == SqlKind.AS && (i == 0)) {
+ // for an aliased expression, it is under FROM if
+ // the AS expression is under FROM
+ childUnderFrom = underFrom;
+ } else {
+ childUnderFrom = false;
+ }
+ newOperand =
+ performUnconditionalRewrites(operand, childUnderFrom);
+ if (newOperand != null && newOperand != operand) {
+ call.setOperand(i, newOperand);
+ }
+ }
+
+ if (call.getOperator() instanceof SqlUnresolvedFunction) {
+ assert call instanceof SqlBasicCall;
+ final SqlUnresolvedFunction function =
+ (SqlUnresolvedFunction) call.getOperator();
+ // This function hasn't been resolved yet. Perform
+ // a half-hearted resolution now in case it's a
+ // builtin function requiring special casing. If it's
+ // not, we'll handle it later during overload resolution.
+ final List<SqlOperator> overloads = new ArrayList<>();
+ opTab.lookupOperatorOverloads(function.getNameAsId(),
+ function.getFunctionType(), SqlSyntax.FUNCTION, overloads,
+ catalogReader.nameMatcher());
+ if (overloads.size() == 1) {
+ ((SqlBasicCall) call).setOperator(overloads.get(0));
+ }
+ }
+ if (rewriteCalls) {
+ node = call.getOperator().rewriteCall(this, call);
+ }
+ } else if (node instanceof SqlNodeList) {
+ SqlNodeList list = (SqlNodeList) node;
+ for (int i = 0, count = list.size(); i < count; i++) {
+ SqlNode operand = list.get(i);
+ newOperand =
+ performUnconditionalRewrites(
+ operand,
+ false);
+ if (newOperand != null) {
+ list.getList().set(i, newOperand);
+ }
+ }
+ }
+
+ // now transform node itself
+ final SqlKind kind = node.getKind();
+ switch (kind) {
+ case VALUES:
+ // CHECKSTYLE: IGNORE 1
+ if (underFrom || true) {
+ // leave FROM (VALUES(...)) [ AS alias ] clauses alone,
+ // otherwise they grow cancerously if this rewrite is invoked
+ // over and over
+ return node;
+ } else {
+ final SqlNodeList selectList =
+ new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ return new SqlSelect(node.getParserPosition(), null, selectList, node,
+ null, null, null, null, null, null, null);
+ }
+
+ case ORDER_BY: {
+ SqlOrderBy orderBy = (SqlOrderBy) node;
+ handleOffsetFetch(orderBy.offset, orderBy.fetch);
+ if (orderBy.query instanceof SqlSelect) {
+ SqlSelect select = (SqlSelect) orderBy.query;
+
+ // Don't clobber existing ORDER BY. It may be needed for
+ // an order-sensitive function like RANK.
+ if (select.getOrderList() == null) {
+ // push ORDER BY into existing select
+ select.setOrderBy(orderBy.orderList);
+ select.setOffset(orderBy.offset);
+ select.setFetch(orderBy.fetch);
+ return select;
+ }
+ }
+ if (orderBy.query instanceof SqlWith
+ && ((SqlWith) orderBy.query).body instanceof SqlSelect) {
+ SqlWith with = (SqlWith) orderBy.query;
+ SqlSelect select = (SqlSelect) with.body;
+
+ // Don't clobber existing ORDER BY. It may be needed for
+ // an order-sensitive function like RANK.
+ if (select.getOrderList() == null) {
+ // push ORDER BY into existing select
+ select.setOrderBy(orderBy.orderList);
+ select.setOffset(orderBy.offset);
+ select.setFetch(orderBy.fetch);
+ return with;
+ }
+ }
+ final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ final SqlNodeList orderList;
+ if (getInnerSelect(node) != null && isAggregate(getInnerSelect(node))) {
+ orderList = SqlNode.clone(orderBy.orderList);
+ // We assume that ORDER BY item does not have ASC etc.
+ // We assume that ORDER BY item is present in SELECT list.
+ for (int i = 0; i < orderList.size(); i++) {
+ SqlNode sqlNode = orderList.get(i);
+ SqlNodeList selectList2 = getInnerSelect(node).getSelectList();
+ for (Ord<SqlNode> sel : Ord.zip(selectList2)) {
+ if (stripAs(sel.e).equalsDeep(sqlNode, Litmus.IGNORE)) {
+ orderList.set(i,
+ SqlLiteral.createExactNumeric(Integer.toString(sel.i + 1),
+ SqlParserPos.ZERO));
+ }
+ }
+ }
+ } else {
+ orderList = orderBy.orderList;
+ }
+ return new SqlSelect(SqlParserPos.ZERO, null, selectList, orderBy.query,
+ null, null, null, null, orderList, orderBy.offset,
+ orderBy.fetch);
+ }
+
+ case EXPLICIT_TABLE: {
+ // (TABLE t) is equivalent to (SELECT * FROM t)
+ SqlCall call = (SqlCall) node;
+ final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ return new SqlSelect(SqlParserPos.ZERO, null, selectList, call.operand(0),
+ null, null, null, null, null, null, null);
+ }
+
+ case DELETE: {
+ SqlDelete call = (SqlDelete) node;
+ SqlSelect select = createSourceSelectForDelete(call);
+ call.setSourceSelect(select);
+ break;
+ }
+
+ case UPDATE: {
+ SqlUpdate call = (SqlUpdate) node;
+ SqlSelect select = createSourceSelectForUpdate(call);
+ call.setSourceSelect(select);
+
+ // See if we're supposed to rewrite UPDATE to MERGE
+ // (unless this is the UPDATE clause of a MERGE,
+ // in which case leave it alone).
+ if (!validatingSqlMerge) {
+ SqlNode selfJoinSrcExpr =
+ getSelfJoinExprForUpdate(
+ call.getTargetTable(),
+ UPDATE_SRC_ALIAS);
+ if (selfJoinSrcExpr != null) {
+ node = rewriteUpdateToMerge(call, selfJoinSrcExpr);
+ }
+ }
+ break;
+ }
+
+ case MERGE: {
+ SqlMerge call = (SqlMerge) node;
+ rewriteMerge(call);
+ break;
+ }
+ }
+ return node;
+ }
+
+ private SqlSelect getInnerSelect(SqlNode node) {
+ for (;;) {
+ if (node instanceof SqlSelect) {
+ return (SqlSelect) node;
+ } else if (node instanceof SqlOrderBy) {
+ node = ((SqlOrderBy) node).query;
+ } else if (node instanceof SqlWith) {
+ node = ((SqlWith) node).body;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private void rewriteMerge(SqlMerge call) {
+ SqlNodeList selectList;
+ SqlUpdate updateStmt = call.getUpdateCall();
+ if (updateStmt != null) {
+ // if we have an update statement, just clone the select list
+ // from the update statement's source since it's the same as
+ // what we want for the select list of the merge source -- '*'
+ // followed by the update set expressions
+ selectList = SqlNode.clone(updateStmt.getSourceSelect().getSelectList());
+ } else {
+ // otherwise, just use select *
+ selectList = new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ }
+ SqlNode targetTable = call.getTargetTable();
+ if (call.getAlias() != null) {
+ targetTable =
+ SqlValidatorUtil.addAlias(
+ targetTable,
+ call.getAlias().getSimple());
+ }
+
+ // Provided there is an insert substatement, the source select for
+ // the merge is a left outer join between the source in the USING
+ // clause and the target table; otherwise, the join is just an
+ // inner join. Need to clone the source table reference in order
+ // for validation to work
+ SqlNode sourceTableRef = call.getSourceTableRef();
+ SqlInsert insertCall = call.getInsertCall();
+ JoinType joinType = (insertCall == null) ? JoinType.INNER : JoinType.LEFT;
+ final SqlNode leftJoinTerm = SqlNode.clone(sourceTableRef);
+ SqlNode outerJoin =
+ new SqlJoin(SqlParserPos.ZERO,
+ leftJoinTerm,
+ SqlLiteral.createBoolean(false, SqlParserPos.ZERO),
+ joinType.symbol(SqlParserPos.ZERO),
+ targetTable,
+ JoinConditionType.ON.symbol(SqlParserPos.ZERO),
+ call.getCondition());
+ SqlSelect select =
+ new SqlSelect(SqlParserPos.ZERO, null, selectList, outerJoin, null,
+ null, null, null, null, null, null);
+ call.setSourceSelect(select);
+
+ // Source for the insert call is a select of the source table
+ // reference with the select list being the value expressions;
+ // note that the values clause has already been converted to a
+ // select on the values row constructor; so we need to extract
+ // that via the from clause on the select
+ if (insertCall != null) {
+ SqlCall valuesCall = (SqlCall) insertCall.getSource();
+ SqlCall rowCall = valuesCall.operand(0);
+ selectList =
+ new SqlNodeList(
+ rowCall.getOperandList(),
+ SqlParserPos.ZERO);
+ final SqlNode insertSource = SqlNode.clone(sourceTableRef);
+ select =
+ new SqlSelect(SqlParserPos.ZERO, null, selectList, insertSource, null,
+ null, null, null, null, null, null);
+ insertCall.setSource(select);
+ }
+ }
+
+ private SqlNode rewriteUpdateToMerge(
+ SqlUpdate updateCall,
+ SqlNode selfJoinSrcExpr) {
+ // Make sure target has an alias.
+ if (updateCall.getAlias() == null) {
+ updateCall.setAlias(
+ new SqlIdentifier(UPDATE_TGT_ALIAS, SqlParserPos.ZERO));
+ }
+ SqlNode selfJoinTgtExpr =
+ getSelfJoinExprForUpdate(
+ updateCall.getTargetTable(),
+ updateCall.getAlias().getSimple());
+ assert selfJoinTgtExpr != null;
+
+ // Create join condition between source and target exprs,
+ // creating a conjunction with the user-level WHERE
+ // clause if one was supplied
+ SqlNode condition = updateCall.getCondition();
+ SqlNode selfJoinCond =
+ SqlStdOperatorTable.EQUALS.createCall(
+ SqlParserPos.ZERO,
+ selfJoinSrcExpr,
+ selfJoinTgtExpr);
+ if (condition == null) {
+ condition = selfJoinCond;
+ } else {
+ condition =
+ SqlStdOperatorTable.AND.createCall(
+ SqlParserPos.ZERO,
+ selfJoinCond,
+ condition);
+ }
+ SqlNode target =
+ updateCall.getTargetTable().clone(SqlParserPos.ZERO);
+
+ // For the source, we need to anonymize the fields, so
+ // that for a statement like UPDATE T SET I = I + 1,
+ // there's no ambiguity for the "I" in "I + 1";
+ // this is OK because the source and target have
+ // identical values due to the self-join.
+ // Note that we anonymize the source rather than the
+ // target because downstream, the optimizer rules
+ // don't want to see any projection on top of the target.
+ IdentifierNamespace ns =
+ new IdentifierNamespace(this, target, null, null);
+ RelDataType rowType = ns.getRowType();
+ SqlNode source = updateCall.getTargetTable().clone(SqlParserPos.ZERO);
+ final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
+ int i = 1;
+ for (RelDataTypeField field : rowType.getFieldList()) {
+ SqlIdentifier col =
+ new SqlIdentifier(
+ field.getName(),
+ SqlParserPos.ZERO);
+ selectList.add(
+ SqlValidatorUtil.addAlias(col, UPDATE_ANON_PREFIX + i));
+ ++i;
+ }
+ source =
+ new SqlSelect(SqlParserPos.ZERO, null, selectList, source, null, null,
+ null, null, null, null, null);
+ source = SqlValidatorUtil.addAlias(source, UPDATE_SRC_ALIAS);
+ SqlMerge mergeCall =
+ new SqlMerge(updateCall.getParserPosition(), target, condition, source,
+ updateCall, null, null, updateCall.getAlias());
+ rewriteMerge(mergeCall);
+ return mergeCall;
+ }
+
+ /**
+ * Allows a subclass to provide information about how to convert an UPDATE
+ * into a MERGE via self-join. If this method returns null, then no such
+ * conversion takes place. Otherwise, this method should return a suitable
+ * unique identifier expression for the given table.
+ *
+ * @param table identifier for table being updated
+ * @param alias alias to use for qualifying columns in expression, or null
+ * for unqualified references; if this is equal to
+ * {@value #UPDATE_SRC_ALIAS}, then column references have been
+ * anonymized to "SYS$ANONx", where x is the 1-based column
+ * number.
+ * @return expression for unique identifier, or null to prevent conversion
+ */
+ protected SqlNode getSelfJoinExprForUpdate(
+ SqlNode table,
+ String alias) {
+ return null;
+ }
+
+ /**
+ * Creates the SELECT statement that putatively feeds rows into an UPDATE
+ * statement to be updated.
+ *
+ * @param call Call to the UPDATE operator
+ * @return select statement
+ */
+ protected SqlSelect createSourceSelectForUpdate(SqlUpdate call) {
+ final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ int ordinal = 0;
+ for (SqlNode exp : call.getSourceExpressionList()) {
+ // Force unique aliases to avoid a duplicate for Y with
+ // SET X=Y
+ String alias = SqlUtil.deriveAliasFromOrdinal(ordinal);
+ selectList.add(SqlValidatorUtil.addAlias(exp, alias));
+ ++ordinal;
+ }
+ SqlNode sourceTable = call.getTargetTable();
+ if (call.getAlias() != null) {
+ sourceTable =
+ SqlValidatorUtil.addAlias(
+ sourceTable,
+ call.getAlias().getSimple());
+ }
+ return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
+ call.getCondition(), null, null, null, null, null, null);
+ }
+
+ /**
+ * Creates the SELECT statement that putatively feeds rows into a DELETE
+ * statement to be deleted.
+ *
+ * @param call Call to the DELETE operator
+ * @return select statement
+ */
+ protected SqlSelect createSourceSelectForDelete(SqlDelete call) {
+ final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
+ selectList.add(SqlIdentifier.star(SqlParserPos.ZERO));
+ SqlNode sourceTable = call.getTargetTable();
+ if (call.getAlias() != null) {
+ sourceTable =
+ SqlValidatorUtil.addAlias(
+ sourceTable,
+ call.getAlias().getSimple());
+ }
+ return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
+ call.getCondition(), null, null, null, null, null, null);
+ }
+
+ /**
+ * Returns null if there is no common type. E.g. if the rows have a
+ * different number of columns.
+ */
+ RelDataType getTableConstructorRowType(
+ SqlCall values,
+ SqlValidatorScope scope) {
+ final List<SqlNode> rows = values.getOperandList();
+ assert rows.size() >= 1;
+ final List<RelDataType> rowTypes = new ArrayList<>();
+ for (final SqlNode row : rows) {
+ assert row.getKind() == SqlKind.ROW;
+ SqlCall rowConstructor = (SqlCall) row;
+
+ // REVIEW jvs 10-Sept-2003: Once we support single-row queries as
+ // rows, need to infer aliases from there.
+ final List<String> aliasList = new ArrayList<>();
+ final List<RelDataType> typeList = new ArrayList<>();
+ for (Ord<SqlNode> column : Ord.zip(rowConstructor.getOperandList())) {
+ final String alias = deriveAlias(column.e, column.i);
+ aliasList.add(alias);
+ final RelDataType type = deriveType(scope, column.e);
+ typeList.add(type);
+ }
+ rowTypes.add(typeFactory.createStructType(typeList, aliasList));
+ }
+ if (rows.size() == 1) {
+ // TODO jvs 10-Oct-2005: get rid of this workaround once
+ // leastRestrictive can handle all cases
+ return rowTypes.get(0);
+ }
+ return typeFactory.leastRestrictive(rowTypes);
+ }
+
+ public RelDataType getValidatedNodeType(SqlNode node) {
+ RelDataType type = getValidatedNodeTypeIfKnown(node);
+ if (type == null) {
+ throw Util.needToImplement(node);
+ } else {
+ return type;
+ }
+ }
+
+ public RelDataType getValidatedNodeTypeIfKnown(SqlNode node) {
+ final RelDataType type = nodeToTypeMap.get(node);
+ if (type != null) {
+ return type;
+ }
+ final SqlValidatorNamespace ns = getNamespace(node);
+ if (ns != null) {
+ return ns.getType();
+ }
+ final SqlNode original = originalExprs.get(node);
+ if (original != null && original != node) {
+ return getValidatedNodeType(original);
+ }
+ if (node instanceof SqlIdentifier) {
+ return getCatalogReader().getNamedType((SqlIdentifier) node);
+ }
+ return null;
+ }
+
+ /**
+ * Saves the type of a {@link SqlNode}, now that it has been validated.
+ *
+ * <p>Unlike the base class method, this method is not deprecated.
+ * It is available from within Calcite, but is not part of the public API.
+ *
+ * @param node A SQL parse tree node, never null
+ * @param type Its type; must not be null
+ */
+ @SuppressWarnings("deprecation")
+ public final void setValidatedNodeType(SqlNode node, RelDataType type) {
+ Objects.requireNonNull(type);
+ Objects.requireNonNull(node);
+ if (type.equals(unknownType)) {
+ // don't set anything until we know what it is, and don't overwrite
+ // a known type with the unknown type
+ return;
+ }
+ nodeToTypeMap.put(node, type);
+ }
+
+ public void removeValidatedNodeType(SqlNode node) {
+ nodeToTypeMap.remove(node);
+ }
+
+ @Nullable public SqlCall makeNullaryCall(SqlIdentifier id) {
+ if (id.names.size() == 1 && !id.isComponentQuoted(0)) {
+ final List<SqlOperator> list = new ArrayList<>();
+ opTab.lookupOperatorOverloads(id, null, SqlSyntax.FUNCTION, list,
+ catalogReader.nameMatcher());
+ for (SqlOperator operator : list) {
+ if (operator.getSyntax() == SqlSyntax.FUNCTION_ID) {
+ // Even though this looks like an identifier, it is a
+ // actually a call to a function. Construct a fake
+ // call to this function, so we can use the regular
+ // operator validation.
+ return new SqlBasicCall(operator, SqlNode.EMPTY_ARRAY,
+ id.getParserPosition(), true, null);
+ }
+ }
+ }
+ return null;
+ }
+
+ public RelDataType deriveType(
+ SqlValidatorScope scope,
+ SqlNode expr) {
+ Objects.requireNonNull(scope);
+ Objects.requireNonNull(expr);
+
+ // if we already know the type, no need to re-derive
+ RelDataType type = nodeToTypeMap.get(expr);
+ if (type != null) {
+ return type;
+ }
+ final SqlValidatorNamespace ns = getNamespace(expr);
+ if (ns != null) {
+ return ns.getType();
+ }
+ type = deriveTypeImpl(scope, expr);
+ Preconditions.checkArgument(
+ type != null,
+ "SqlValidator.deriveTypeInternal returned null");
+ setValidatedNodeType(expr, type);
+ return type;
+ }
+
+ /**
+ * Derives the type of a node, never null.
+ */
+ RelDataType deriveTypeImpl(
+ SqlValidatorScope scope,
+ SqlNode operand) {
+ DeriveTypeVisitor v = new DeriveTypeVisitor(scope);
+ final RelDataType type = operand.accept(v);
+ return Objects.requireNonNull(scope.nullifyType(operand, type));
+ }
+
+ public RelDataType deriveConstructorType(
+ SqlValidatorScope scope,
+ SqlCall call,
+ SqlFunction unresolvedConstructor,
+ SqlFunction resolvedConstructor,
+ List<RelDataType> argTypes) {
+ SqlIdentifier sqlIdentifier = unresolvedConstructor.getSqlIdentifier();
+ assert sqlIdentifier != null;
+ RelDataType type = catalogReader.getNamedType(sqlIdentifier);
+ if (type == null) {
+ // TODO jvs 12-Feb-2005: proper type name formatting
+ throw newValidationError(sqlIdentifier,
+ RESOURCE.unknownDatatypeName(sqlIdentifier.toString()));
+ }
+
+ if (resolvedConstructor == null) {
+ if (call.operandCount() > 0) {
+ // This is not a default constructor invocation, and
+ // no user-defined constructor could be found
+ throw handleUnresolvedFunction(call, unresolvedConstructor, argTypes,
+ null);
+ }
+ } else {
+ SqlCall testCall =
+ resolvedConstructor.createCall(
+ call.getParserPosition(),
+ call.getOperandList());
+ RelDataType returnType =
+ resolvedConstructor.validateOperands(
+ this,
+ scope,
+ testCall);
+ assert type == returnType;
+ }
+
+ if (shouldExpandIdentifiers()) {
+ if (resolvedConstructor != null) {
+ ((SqlBasicCall) call).setOperator(resolvedConstructor);
+ } else {
+ // fake a fully-qualified call to the default constructor
+ ((SqlBasicCall) call).setOperator(
+ new SqlFunction(
+ type.getSqlIdentifier(),
+ ReturnTypes.explicit(type),
+ null,
+ null,
+ null,
+ SqlFunctionCategory.USER_DEFINED_CONSTRUCTOR));
+ }
+ }
+ return type;
+ }
+
+ public CalciteException handleUnresolvedFunction(SqlCall call,
+ SqlFunction unresolvedFunction, List<RelDataType> argTypes,
+ List<String> argNames) {
+ // For builtins, we can give a better error message
+ final List<SqlOperator> overloads = new ArrayList<>();
+ opTab.lookupOperatorOverloads(unresolvedFunction.getNameAsId(), null,
+ SqlSyntax.FUNCTION, overloads, catalogReader.nameMatcher());
+ if (overloads.size() == 1) {
+ SqlFunction fun = (SqlFunction) overloads.get(0);
+ if ((fun.getSqlIdentifier() == null)
+ && (fun.getSyntax() != SqlSyntax.FUNCTION_ID)) {
+ final int expectedArgCount =
+ fun.getOperandCountRange().getMin();
+ throw newValidationError(call,
+ RESOURCE.invalidArgCount(call.getOperator().getName(),
+ expectedArgCount));
+ }
+ }
+
+ AssignableOperandTypeChecker typeChecking =
+ new AssignableOperandTypeChecker(argTypes, argNames);
+ String signature =
+ typeChecking.getAllowedSignatures(
+ unresolvedFunction,
+ unresolvedFunction.getName());
+ throw newValidationError(call,
+ RESOURCE.validatorUnknownFunction(signature));
+ }
+
+ protected void inferUnknownTypes(
+ @Nonnull RelDataType inferredType,
+ @Nonnull SqlValidatorScope scope,
+ @Nonnull SqlNode node) {
+ Objects.requireNonNull(inferredType);
+ Objects.requireNonNull(scope);
+ Objects.requireNonNull(node);
+ final SqlValidatorScope newScope = scopes.get(node);
+ if (newScope != null) {
+ scope = newScope;
+ }
+ boolean isNullLiteral = SqlUtil.isNullLiteral(node, false);
+ if ((node instanceof SqlDynamicParam) || isNullLiteral) {
+ if (inferredType.equals(unknownType)) {
+ if (isNullLiteral) {
+ throw newValidationError(node, RESOURCE.nullIllegal());
+ } else {
+ throw newValidationError(node, RESOURCE.dynamicParamIllegal());
+ }
+ }
+
+ // REVIEW: should dynamic parameter types always be nullable?
+ RelDataType newInferredType =
+ typeFactory.createTypeWithNullability(inferredType, true);
+ if (SqlTypeUtil.inCharFamily(inferredType)) {
+ newInferredType =
+ typeFactory.createTypeWithCharsetAndCollation(
+ newInferredType,
+ inferredType.getCharset(),
+ inferredType.getCollation());
+ }
+ setValidatedNodeType(node, newInferredType);
+ } else if (node instanceof SqlNodeList) {
+ SqlNodeList nodeList = (SqlNodeList) node;
+ if (inferredType.isStruct()) {
+ if (inferredType.getFieldCount() != nodeList.size()) {
+ // this can happen when we're validating an INSERT
+ // where the source and target degrees are different;
+ // bust out, and the error will be detected higher up
+ return;
+ }
+ }
+ int i = 0;
+ for (SqlNode child : nodeList) {
+ RelDataType type;
+ if (inferredType.isStruct()) {
+ type = inferredType.getFieldList().get(i).getType();
+ ++i;
+ } else {
+ type = inferredType;
+ }
+ inferUnknownTypes(type, scope, child);
+ }
+ } else if (node instanceof SqlCase) {
+ final SqlCase caseCall = (SqlCase) node;
+
+ final RelDataType whenType =
+ caseCall.getValueOperand() == null ? booleanType : unknownType;
+ for (SqlNode sqlNode : caseCall.getWhenOperands().getList()) {
+ inferUnknownTypes(whenType, scope, sqlNode);
+ }
+ RelDataType returnType = deriveType(scope, node);
+ for (SqlNode sqlNode : caseCall.getThenOperands().getList()) {
+ inferUnknownTypes(returnType, scope, sqlNode);
+ }
+
+ if (!SqlUtil.isNullLiteral(caseCall.getElseOperand(), false)) {
+ inferUnknownTypes(
+ returnType,
+ scope,
+ caseCall.getElseOperand());
+ } else {
+ setValidatedNodeType(caseCall.getElseOperand(), returnType);
+ }
+ } else if (node.getKind() == SqlKind.AS) {
+ // For AS operator, only infer the operand not the alias
+ inferUnknownTypes(inferredType, scope, ((SqlCall) node).operand(0));
+ } else if (node instanceof SqlCall) {
+ final SqlCall call = (SqlCall) node;
+ final SqlOperandTypeInference operandTypeInference =
+ call.getOperator().getOperandTypeInference();
+ final SqlCallBinding callBinding = new SqlCallBinding(this, scope, call);
+ final List<SqlNode> operands = callBinding.operands();
+ final RelDataType[] operandTypes = new RelDataType[operands.size()];
+ Arrays.fill(operandTypes, unknownType);
+ // TODO: eventually should assert(operandTypeInference != null)
+ // instead; for now just eat it
+ if (operandTypeInference != null) {
+ operandTypeInference.inferOperandTypes(
+ callBinding,
+ inferredType,
+ operandTypes);
+ }
+ for (int i = 0; i < operands.size(); ++i) {
+ final SqlNode operand = operands.get(i);
+ if (operand != null) {
+ inferUnknownTypes(operandTypes[i], scope, operand);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds an expression to a select list, ensuring that its alias does not
+ * clash with any existing expressions on the list.
+ */
+ protected void addToSelectList(
+ List<SqlNode> list,
+ Set<String> aliases,
+ List<Map.Entry<String, RelDataType>> fieldList,
+ SqlNode exp,
+ SqlValidatorScope scope,
+ final boolean includeSystemVars) {
+ String alias = SqlValidatorUtil.getAlias(exp, -1);
+ String uniqueAlias =
+ SqlValidatorUtil.uniquify(
+ alias, aliases, SqlValidatorUtil.EXPR_SUGGESTER);
+ if (!alias.equals(uniqueAlias)) {
+ exp = SqlValidatorUtil.addAlias(exp, uniqueAlias);
+ }
+ fieldList.add(Pair.of(uniqueAlias, deriveType(scope, exp)));
+ list.add(exp);
+ }
+
+ public String deriveAlias(
+ SqlNode node,
+ int ordinal) {
+ return SqlValidatorUtil.getAlias(node, ordinal);
+ }
+
+ // implement SqlValidator
+ public void setIdentifierExpansion(boolean expandIdentifiers) {
+ this.expandIdentifiers = expandIdentifiers;
+ }
+
+ // implement SqlValidator
+ public void setColumnReferenceExpansion(
+ boolean expandColumnReferences) {
+ this.expandColumnReferences = expandColumnReferences;
+ }
+
+ // implement SqlValidator
+ public boolean getColumnReferenceExpansion() {
+ return expandColumnReferences;
+ }
+
+ public void setDefaultNullCollation(NullCollation nullCollation) {
+ this.nullCollation = Objects.requireNonNull(nullCollation);
+ }
+
+ public NullCollation getDefaultNullCollation() {
+ return nullCollation;
+ }
+
+ // implement SqlValidator
+ public void setCallRewrite(boolean rewriteCalls) {
+ this.rewriteCalls = rewriteCalls;
+ }
+
+ public boolean shouldExpandIdentifiers() {
+ return expandIdentifiers;
+ }
+
+ protected boolean shouldAllowIntermediateOrderBy() {
+ return true;
+ }
+
+ private void registerMatchRecognize(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ SqlMatchRecognize call,
+ SqlNode enclosingNode,
+ String alias,
+ boolean forceNullable) {
+
+ final MatchRecognizeNamespace matchRecognizeNamespace =
+ createMatchRecognizeNameSpace(call, enclosingNode);
+ registerNamespace(usingScope, alias, matchRecognizeNamespace, forceNullable);
+
+ final MatchRecognizeScope matchRecognizeScope =
+ new MatchRecognizeScope(parentScope, call);
+ scopes.put(call, matchRecognizeScope);
+
+ // parse input query
+ SqlNode expr = call.getTableRef();
+ SqlNode newExpr = registerFrom(usingScope, matchRecognizeScope, true, expr,
+ expr, null, null, forceNullable, false);
+ if (expr != newExpr) {
+ call.setOperand(0, newExpr);
+ }
+ }
+
+ protected MatchRecognizeNamespace createMatchRecognizeNameSpace(
+ SqlMatchRecognize call,
+ SqlNode enclosingNode) {
+ return new MatchRecognizeNamespace(this, call, enclosingNode);
+ }
+
+ /**
+ * Registers a new namespace, and adds it as a child of its parent scope.
+ * Derived class can override this method to tinker with namespaces as they
+ * are created.
+ *
+ * @param usingScope Parent scope (which will want to look for things in
+ * this namespace)
+ * @param alias Alias by which parent will refer to this namespace
+ * @param ns Namespace
+ * @param forceNullable Whether to force the type of namespace to be nullable
+ */
+ protected void registerNamespace(
+ SqlValidatorScope usingScope,
+ String alias,
+ SqlValidatorNamespace ns,
+ boolean forceNullable) {
+ namespaces.put(ns.getNode(), ns);
+ if (usingScope != null) {
+ usingScope.addChild(ns, alias, forceNullable);
+ }
+ }
+
+ /**
+ * Registers scopes and namespaces implied a relational expression in the
+ * FROM clause.
+ *
+ * <p>{@code parentScope} and {@code usingScope} are often the same. They
+ * differ when the namespace are not visible within the parent. (Example
+ * needed.)
+ *
+ * <p>Likewise, {@code enclosingNode} and {@code node} are often the same.
+ * {@code enclosingNode} is the topmost node within the FROM clause, from
+ * which any decorations like an alias (<code>AS alias</code>) or a table
+ * sample clause are stripped away to get {@code node}. Both are recorded in
+ * the namespace.
+ *
+ * @param parentScope Parent scope which this scope turns to in order to
+ * resolve objects
+ * @param usingScope Scope whose child list this scope should add itself to
+ * @param register Whether to register this scope as a child of
+ * {@code usingScope}
+ * @param node Node which namespace is based on
+ * @param enclosingNode Outermost node for namespace, including decorations
+ * such as alias and sample clause
+ * @param alias Alias
+ * @param extendList Definitions of extended columns
+ * @param forceNullable Whether to force the type of namespace to be
+ * nullable because it is in an outer join
+ * @param lateral Whether LATERAL is specified, so that items to the
+ * left of this in the JOIN tree are visible in the
+ * scope
+ * @return registered node, usually the same as {@code node}
+ */
+ private SqlNode registerFrom(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ boolean register,
+ final SqlNode node,
+ SqlNode enclosingNode,
+ String alias,
+ SqlNodeList extendList,
+ boolean forceNullable,
+ final boolean lateral) {
+ final SqlKind kind = node.getKind();
+
+ SqlNode expr;
+ SqlNode newExpr;
+
+ // Add an alias if necessary.
+ SqlNode newNode = node;
+ if (alias == null) {
+ switch (kind) {
+ case IDENTIFIER:
+ case OVER:
+ alias = deriveAlias(node, -1);
+ if (alias == null) {
+ alias = deriveAlias(node, nextGeneratedId++);
+ }
+ if (shouldExpandIdentifiers()) {
+ newNode = SqlValidatorUtil.addAlias(node, alias);
+ }
+ break;
+
+ case SELECT:
+ case UNION:
+ case INTERSECT:
+ case EXCEPT:
+ case VALUES:
+ case UNNEST:
+ case OTHER_FUNCTION:
+ case COLLECTION_TABLE:
+ case MATCH_RECOGNIZE:
+
+ // give this anonymous construct a name since later
+ // query processing stages rely on it
+ alias = deriveAlias(node, nextGeneratedId++);
+ if (shouldExpandIdentifiers()) {
+ // Since we're expanding identifiers, we should make the
+ // aliases explicit too, otherwise the expanded query
+ // will not be consistent if we convert back to SQL, e.g.
+ // "select EXPR$1.EXPR$2 from values (1)".
+ newNode = SqlValidatorUtil.addAlias(node, alias);
+ }
+ break;
+ }
+ }
+
+ if (lateral) {
+ SqlValidatorScope s = usingScope;
+ while (s instanceof JoinScope) {
+ s = ((JoinScope) s).getUsingScope();
+ }
+ final SqlNode node2 = s != null ? s.getNode() : node;
+ final TableScope tableScope = new TableScope(parentScope, node2);
+ if (usingScope instanceof ListScope) {
+ for (ScopeChild child : ((ListScope) usingScope).children) {
+ tableScope.addChild(child.namespace, child.name, child.nullable);
+ }
+ }
+ parentScope = tableScope;
+ }
+
+ SqlCall call;
+ SqlNode operand;
+ SqlNode newOperand;
+
+ switch (kind) {
+ case AS:
+ call = (SqlCall) node;
+ if (alias == null) {
+ alias = call.operand(1).toString();
+ }
+ final boolean needAlias = call.operandCount() > 2;
+ expr = call.operand(0);
+ newExpr =
+ registerFrom(
+ parentScope,
+ usingScope,
+ !needAlias,
+ expr,
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ lateral);
+ if (newExpr != expr) {
+ call.setOperand(0, newExpr);
+ }
+
+ // If alias has a column list, introduce a namespace to translate
+ // column names. We skipped registering it just now.
+ if (needAlias) {
+ registerNamespace(
+ usingScope,
+ alias,
+ new AliasNamespace(this, call, enclosingNode),
+ forceNullable);
+ }
+ return node;
+ case MATCH_RECOGNIZE:
+ registerMatchRecognize(parentScope, usingScope,
+ (SqlMatchRecognize) node, enclosingNode, alias, forceNullable);
+ return node;
+ case TABLESAMPLE:
+ call = (SqlCall) node;
+ expr = call.operand(0);
+ newExpr =
+ registerFrom(
+ parentScope,
+ usingScope,
+ true,
+ expr,
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ lateral);
+ if (newExpr != expr) {
+ call.setOperand(0, newExpr);
+ }
+ return node;
+
+ case JOIN:
+ final SqlJoin join = (SqlJoin) node;
+ final JoinScope joinScope =
+ new JoinScope(parentScope, usingScope, join);
+ scopes.put(join, joinScope);
+ final SqlNode left = join.getLeft();
+ final SqlNode right = join.getRight();
+ final boolean rightIsLateral = isLateral(right);
+ boolean forceLeftNullable = forceNullable;
+ boolean forceRightNullable = forceNullable;
+ switch (join.getJoinType()) {
+ case LEFT:
+ forceRightNullable = true;
+ break;
+ case RIGHT:
+ forceLeftNullable = true;
+ break;
+ case FULL:
+ forceLeftNullable = true;
+ forceRightNullable = true;
+ break;
+ }
+ final SqlNode newLeft =
+ registerFrom(
+ parentScope,
+ joinScope,
+ true,
+ left,
+ left,
+ null,
+ null,
+ forceLeftNullable,
+ lateral);
+ if (newLeft != left) {
+ join.setLeft(newLeft);
+ }
+ final SqlNode newRight =
+ registerFrom(
+ parentScope,
+ joinScope,
+ true,
+ right,
+ right,
+ null,
+ null,
+ forceRightNullable,
+ lateral);
+ if (newRight != right) {
+ join.setRight(newRight);
+ }
+ registerSubQueries(joinScope, join.getCondition());
+ final JoinNamespace joinNamespace = new JoinNamespace(this, join);
+ registerNamespace(null, null, joinNamespace, forceNullable);
+ return join;
+
+ case IDENTIFIER:
+ final SqlIdentifier id = (SqlIdentifier) node;
+ final IdentifierNamespace newNs =
+ new IdentifierNamespace(
+ this, id, extendList, enclosingNode,
+ parentScope);
+ registerNamespace(register ? usingScope : null, alias, newNs,
+ forceNullable);
+ if (tableScope == null) {
+ tableScope = new TableScope(parentScope, node);
+ }
+ tableScope.addChild(newNs, alias, forceNullable);
+ if (extendList != null && extendList.size() != 0) {
+ return enclosingNode;
+ }
+ return newNode;
+
+ case LATERAL:
+ return registerFrom(
+ parentScope,
+ usingScope,
+ register,
+ ((SqlCall) node).operand(0),
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ true);
+
+ case COLLECTION_TABLE:
+ call = (SqlCall) node;
+ operand = call.operand(0);
+ newOperand =
+ registerFrom(
+ parentScope,
+ usingScope,
+ register,
+ operand,
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable, lateral);
+ if (newOperand != operand) {
+ call.setOperand(0, newOperand);
+ }
+ scopes.put(node, parentScope);
+ return newNode;
+
+ case UNNEST:
+ if (!lateral) {
+ return registerFrom(parentScope, usingScope, register, node,
+ enclosingNode, alias, extendList, forceNullable, true);
+ }
+ // fall through
+ case SELECT:
+ case UNION:
+ case INTERSECT:
+ case EXCEPT:
+ case VALUES:
+ case WITH:
+ case OTHER_FUNCTION:
+ if (alias == null) {
+ alias = deriveAlias(node, nextGeneratedId++);
+ }
+ registerQuery(
+ parentScope,
+ register ? usingScope : null,
+ node,
+ enclosingNode,
+ alias,
+ forceNullable);
+ return newNode;
+
+ case OVER:
+ if (!shouldAllowOverRelation()) {
+ throw Util.unexpected(kind);
+ }
+ call = (SqlCall) node;
+ final OverScope overScope = new OverScope(usingScope, call);
+ scopes.put(call, overScope);
+ operand = call.operand(0);
+ newOperand =
+ registerFrom(
+ parentScope,
+ overScope,
+ true,
+ operand,
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ lateral);
+ if (newOperand != operand) {
+ call.setOperand(0, newOperand);
+ }
+
+ for (ScopeChild child : overScope.children) {
+ registerNamespace(register ? usingScope : null, child.name,
+ child.namespace, forceNullable);
+ }
+
+ return newNode;
+
+ case EXTEND:
+ final SqlCall extend = (SqlCall) node;
+ return registerFrom(parentScope,
+ usingScope,
+ true,
+ extend.getOperandList().get(0),
+ extend,
+ alias,
+ (SqlNodeList) extend.getOperandList().get(1),
+ forceNullable,
+ lateral);
+
+ case SNAPSHOT:
+ call = (SqlCall) node;
+ operand = call.operand(0);
+ newOperand = registerFrom(
+ tableScope == null ? parentScope : tableScope,
+ usingScope,
+ register,
+ operand,
+ enclosingNode,
+ alias,
+ extendList,
+ forceNullable,
+ true);
+ if (newOperand != operand) {
+ call.setOperand(0, newOperand);
+ }
+ scopes.put(node, parentScope);
+ return newNode;
+
+ default:
+ throw Util.unexpected(kind);
+ }
+ }
+
+ private static boolean isLateral(SqlNode node) {
+ switch (node.getKind()) {
+ case LATERAL:
+ case UNNEST:
+ // Per SQL std, UNNEST is implicitly LATERAL.
+ return true;
+ case AS:
+ return isLateral(((SqlCall) node).operand(0));
+ default:
+ return false;
+ }
+ }
+
+ protected boolean shouldAllowOverRelation() {
+ return false;
+ }
+
+ /**
+ * Creates a namespace for a <code>SELECT</code> node. Derived class may
+ * override this factory method.
+ *
+ * @param select Select node
+ * @param enclosingNode Enclosing node
+ * @return Select namespace
+ */
+ protected SelectNamespace createSelectNamespace(
+ SqlSelect select,
+ SqlNode enclosingNode) {
+ return new SelectNamespace(this, select, enclosingNode);
+ }
+
+ /**
+ * Creates a namespace for a set operation (<code>UNION</code>, <code>
+ * INTERSECT</code>, or <code>EXCEPT</code>). Derived class may override
+ * this factory method.
+ *
+ * @param call Call to set operation
+ * @param enclosingNode Enclosing node
+ * @return Set operation namespace
+ */
+ protected SetopNamespace createSetopNamespace(
+ SqlCall call,
+ SqlNode enclosingNode) {
+ return new SetopNamespace(this, call, enclosingNode);
+ }
+
+ /**
+ * Registers a query in a parent scope.
+ *
+ * @param parentScope Parent scope which this scope turns to in order to
+ * resolve objects
+ * @param usingScope Scope whose child list this scope should add itself to
+ * @param node Query node
+ * @param alias Name of this query within its parent. Must be specified
+ * if usingScope != null
+ */
+ private void registerQuery(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ SqlNode node,
+ SqlNode enclosingNode,
+ String alias,
+ boolean forceNullable) {
+ Preconditions.checkArgument(usingScope == null || alias != null);
+ registerQuery(
+ parentScope,
+ usingScope,
+ node,
+ enclosingNode,
+ alias,
+ forceNullable,
+ true);
+ }
+
+ /**
+ * Registers a query in a parent scope.
+ *
+ * @param parentScope Parent scope which this scope turns to in order to
+ * resolve objects
+ * @param usingScope Scope whose child list this scope should add itself to
+ * @param node Query node
+ * @param alias Name of this query within its parent. Must be specified
+ * if usingScope != null
+ * @param checkUpdate if true, validate that the update feature is supported
+ * if validating the update statement
+ */
+ private void registerQuery(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ SqlNode node,
+ SqlNode enclosingNode,
+ String alias,
+ boolean forceNullable,
+ boolean checkUpdate) {
+ Objects.requireNonNull(node);
+ Objects.requireNonNull(enclosingNode);
+ Preconditions.checkArgument(usingScope == null || alias != null);
+
+ SqlCall call;
+ List<SqlNode> operands;
+ switch (node.getKind()) {
+ case SELECT:
+ final SqlSelect select = (SqlSelect) node;
+ final SelectNamespace selectNs =
+ createSelectNamespace(select, enclosingNode);
+ registerNamespace(usingScope, alias, selectNs, forceNullable);
+ final SqlValidatorScope windowParentScope =
+ (usingScope != null) ? usingScope : parentScope;
+ SelectScope selectScope =
+ new SelectScope(parentScope, windowParentScope, select);
+ scopes.put(select, selectScope);
+
+ // Start by registering the WHERE clause
+ whereScopes.put(select, selectScope);
+ registerOperandSubQueries(
+ selectScope,
+ select,
+ SqlSelect.WHERE_OPERAND);
+
+ // Register FROM with the inherited scope 'parentScope', not
+ // 'selectScope', otherwise tables in the FROM clause would be
+ // able to see each other.
+ final SqlNode from = select.getFrom();
+ if (from != null) {
+ final SqlNode newFrom =
+ registerFrom(
+ parentScope,
+ selectScope,
+ true,
+ from,
+ from,
+ null,
+ null,
+ false,
+ false);
+ if (newFrom != from) {
+ select.setFrom(newFrom);
+ }
+ }
+
+ // If this is an aggregating query, the SELECT list and HAVING
+ // clause use a different scope, where you can only reference
+ // columns which are in the GROUP BY clause.
+ SqlValidatorScope aggScope = selectScope;
+ if (isAggregate(select)) {
+ aggScope =
+ new AggregatingSelectScope(selectScope, select, false);
+ selectScopes.put(select, aggScope);
+ } else {
+ selectScopes.put(select, selectScope);
+ }
+ if (select.getGroup() != null) {
+ GroupByScope groupByScope =
+ new GroupByScope(selectScope, select.getGroup(), select);
+ groupByScopes.put(select, groupByScope);
+ registerSubQueries(groupByScope, select.getGroup());
+ }
+ registerOperandSubQueries(
+ aggScope,
+ select,
+ SqlSelect.HAVING_OPERAND);
+ registerSubQueries(aggScope, select.getSelectList());
+ final SqlNodeList orderList = select.getOrderList();
+ if (orderList != null) {
+ // If the query is 'SELECT DISTINCT', restrict the columns
+ // available to the ORDER BY clause.
+ if (select.isDistinct()) {
+ aggScope =
+ new AggregatingSelectScope(selectScope, select, true);
+ }
+ OrderByScope orderScope =
+ new OrderByScope(aggScope, orderList, select);
+ orderScopes.put(select, orderScope);
+ registerSubQueries(orderScope, orderList);
+
+ if (!isAggregate(select)) {
+ // Since this is not an aggregating query,
+ // there cannot be any aggregates in the ORDER BY clause.
+ SqlNode agg = aggFinder.findAgg(orderList);
+ if (agg != null) {
+ throw newValidationError(agg, RESOURCE.aggregateIllegalInOrderBy());
+ }
+ }
+ }
+ break;
+
+ case INTERSECT:
+ validateFeature(RESOURCE.sQLFeature_F302(), node.getParserPosition());
+ registerSetop(
+ parentScope,
+ usingScope,
+ node,
+ node,
+ alias,
+ forceNullable);
+ break;
+
+ case EXCEPT:
+ validateFeature(RESOURCE.sQLFeature_E071_03(), node.getParserPosition());
+ registerSetop(
+ parentScope,
+ usingScope,
+ node,
+ node,
+ alias,
+ forceNullable);
+ break;
+
+ case UNION:
+ registerSetop(
+ parentScope,
+ usingScope,
+ node,
+ node,
+ alias,
+ forceNullable);
+ break;
+
+ case WITH:
+ registerWith(parentScope, usingScope, (SqlWith) node, enclosingNode,
+ alias, forceNullable, checkUpdate);
+ break;
+
+ case VALUES:
+ call = (SqlCall) node;
+ scopes.put(call, parentScope);
+ final TableConstructorNamespace tableConstructorNamespace =
+ new TableConstructorNamespace(
+ this,
+ call,
+ parentScope,
+ enclosingNode);
+ registerNamespace(
+ usingScope,
+ alias,
+ tableConstructorNamespace,
+ forceNullable);
+ operands = call.getOperandList();
+ for (int i = 0; i < operands.size(); ++i) {
+ assert operands.get(i).getKind() == SqlKind.ROW;
+
+ // FIXME jvs 9-Feb-2005: Correlation should
+ // be illegal in these sub-queries. Same goes for
+ // any non-lateral SELECT in the FROM list.
+ registerOperandSubQueries(parentScope, call, i);
+ }
+ break;
+
+ case INSERT:
+ SqlInsert insertCall = (SqlInsert) node;
+ InsertNamespace insertNs =
+ new InsertNamespace(
+ this,
+ insertCall,
+ enclosingNode,
+ parentScope);
+ registerNamespace(usingScope, null, insertNs, forceNullable);
+ registerQuery(
+ parentScope,
+ usingScope,
+ insertCall.getSource(),
+ enclosingNode,
+ null,
+ false);
+ break;
+
+ case DELETE:
+ SqlDelete deleteCall = (SqlDelete) node;
+ DeleteNamespace deleteNs =
+ new DeleteNamespace(
+ this,
+ deleteCall,
+ enclosingNode,
+ parentScope);
+ registerNamespace(usingScope, null, deleteNs, forceNullable);
+ registerQuery(
+ parentScope,
+ usingScope,
+ deleteCall.getSourceSelect(),
+ enclosingNode,
+ null,
+ false);
+ break;
+
+ case UPDATE:
+ if (checkUpdate) {
+ validateFeature(RESOURCE.sQLFeature_E101_03(),
+ node.getParserPosition());
+ }
+ SqlUpdate updateCall = (SqlUpdate) node;
+ UpdateNamespace updateNs =
+ new UpdateNamespace(
+ this,
+ updateCall,
+ enclosingNode,
+ parentScope);
+ registerNamespace(usingScope, null, updateNs, forceNullable);
+ registerQuery(
+ parentScope,
+ usingScope,
+ updateCall.getSourceSelect(),
+ enclosingNode,
+ null,
+ false);
+ break;
+
+ case MERGE:
+ validateFeature(RESOURCE.sQLFeature_F312(), node.getParserPosition());
+ SqlMerge mergeCall = (SqlMerge) node;
+ MergeNamespace mergeNs =
+ new MergeNamespace(
+ this,
+ mergeCall,
+ enclosingNode,
+ parentScope);
+ registerNamespace(usingScope, null, mergeNs, forceNullable);
+ registerQuery(
+ parentScope,
+ usingScope,
+ mergeCall.getSourceSelect(),
+ enclosingNode,
+ null,
+ false);
+
+ // update call can reference either the source table reference
+ // or the target table, so set its parent scope to the merge's
+ // source select; when validating the update, skip the feature
+ // validation check
+ if (mergeCall.getUpdateCall() != null) {
+ registerQuery(
+ whereScopes.get(mergeCall.getSourceSelect()),
+ null,
+ mergeCall.getUpdateCall(),
+ enclosingNode,
+ null,
+ false,
+ false);
+ }
+ if (mergeCall.getInsertCall() != null) {
+ registerQuery(
+ parentScope,
+ null,
+ mergeCall.getInsertCall(),
+ enclosingNode,
+ null,
+ false);
+ }
+ break;
+
+ case UNNEST:
+ call = (SqlCall) node;
+ final UnnestNamespace unnestNs =
+ new UnnestNamespace(this, call, parentScope, enclosingNode);
+ registerNamespace(
+ usingScope,
+ alias,
+ unnestNs,
+ forceNullable);
+ registerOperandSubQueries(parentScope, call, 0);
+ scopes.put(node, parentScope);
+ break;
+
+ case OTHER_FUNCTION:
+ call = (SqlCall) node;
+ ProcedureNamespace procNs =
+ new ProcedureNamespace(
+ this,
+ parentScope,
+ call,
+ enclosingNode);
+ registerNamespace(
+ usingScope,
+ alias,
+ procNs,
+ forceNullable);
+ registerSubQueries(parentScope, call);
+ break;
+
+ case MULTISET_QUERY_CONSTRUCTOR:
+ case MULTISET_VALUE_CONSTRUCTOR:
+ validateFeature(RESOURCE.sQLFeature_S271(), node.getParserPosition());
+ call = (SqlCall) node;
+ CollectScope cs = new CollectScope(parentScope, usingScope, call);
+ final CollectNamespace tableConstructorNs =
+ new CollectNamespace(call, cs, enclosingNode);
+ final String alias2 = deriveAlias(node, nextGeneratedId++);
+ registerNamespace(
+ usingScope,
+ alias2,
+ tableConstructorNs,
+ forceNullable);
+ operands = call.getOperandList();
+ for (int i = 0; i < operands.size(); i++) {
+ registerOperandSubQueries(parentScope, call, i);
+ }
+ break;
+
+ default:
+ throw Util.unexpected(node.getKind());
+ }
+ }
+
+ private void registerSetop(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ SqlNode node,
+ SqlNode enclosingNode,
+ String alias,
+ boolean forceNullable) {
+ SqlCall call = (SqlCall) node;
+ final SetopNamespace setopNamespace =
+ createSetopNamespace(call, enclosingNode);
+ registerNamespace(usingScope, alias, setopNamespace, forceNullable);
+
+ // A setop is in the same scope as its parent.
+ scopes.put(call, parentScope);
+ for (SqlNode operand : call.getOperandList()) {
+ registerQuery(
+ parentScope,
+ null,
+ operand,
+ operand,
+ null,
+ false);
+ }
+ }
+
+ private void registerWith(
+ SqlValidatorScope parentScope,
+ SqlValidatorScope usingScope,
+ SqlWith with,
+ SqlNode enclosingNode,
+ String alias,
+ boolean forceNullable,
+ boolean checkUpdate) {
+ final WithNamespace withNamespace =
+ new WithNamespace(this, with, enclosingNode);
+ registerNamespace(usingScope, alias, withNamespace, forceNullable);
+
+ SqlValidatorScope scope = parentScope;
+ for (SqlNode withItem_ : with.withList) {
+ final SqlWithItem withItem = (SqlWithItem) withItem_;
+ final WithScope withScope = new WithScope(scope, withItem);
+ scopes.put(withItem, withScope);
+
+ registerQuery(scope, null, withItem.query, with,
+ withItem.name.getSimple(), false);
+ registerNamespace(null, alias,
+ new WithItemNamespace(this, withItem, enclosingNode),
+ false);
+ scope = withScope;
+ }
+
+ registerQuery(scope, null, with.body, enclosingNode, alias, forceNullable,
+ checkUpdate);
+ }
+
+ public boolean isAggregate(SqlSelect select) {
+ if (getAggregate(select) != null) {
+ return true;
+ }
+ // Also when nested window aggregates are present
+ for (SqlCall call : overFinder.findAll(select.getSelectList())) {
+ assert call.getKind() == SqlKind.OVER;
+ if (isNestedAggregateWindow(call.operand(0))) {
+ return true;
+ }
+ if (isOverAggregateWindow(call.operand(1))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected boolean isNestedAggregateWindow(SqlNode node) {
+ AggFinder nestedAggFinder =
+ new AggFinder(opTab, false, false, false, aggFinder,
+ catalogReader.nameMatcher());
+ return nestedAggFinder.findAgg(node) != null;
+ }
+
+ protected boolean isOverAggregateWindow(SqlNode node) {
+ return aggFinder.findAgg(node) != null;
+ }
+
+ /** Returns the parse tree node (GROUP BY, HAVING, or an aggregate function
+ * call) that causes {@code select} to be an aggregate query, or null if it
+ * is not an aggregate query.
+ *
+ * <p>The node is useful context for error messages,
+ * but you cannot assume that the node is the only aggregate function. */
+ protected SqlNode getAggregate(SqlSelect select) {
+ SqlNode node = select.getGroup();
+ if (node != null) {
+ return node;
+ }
+ node = select.getHaving();
+ if (node != null) {
+ return node;
+ }
+ return getAgg(select);
+ }
+
+ /** If there is at least one call to an aggregate function, returns the
+ * first. */
+ private SqlNode getAgg(SqlSelect select) {
+ final SelectScope selectScope = getRawSelectScope(select);
+ if (selectScope != null) {
+ final List<SqlNode> selectList = selectScope.getExpandedSelectList();
+ if (selectList != null) {
+ return aggFinder.findAgg(selectList);
+ }
+ }
+ return aggFinder.findAgg(select.getSelectList());
+ }
+
+ @SuppressWarnings("deprecation")
+ public boolean isAggregate(SqlNode selectNode) {
+ return aggFinder.findAgg(selectNode) != null;
+ }
+
+ private void validateNodeFeature(SqlNode node) {
+ switch (node.getKind()) {
+ case MULTISET_VALUE_CONSTRUCTOR:
+ validateFeature(RESOURCE.sQLFeature_S271(), node.getParserPosition());
+ break;
+ }
+ }
+
+ private void registerSubQueries(
+ SqlValidatorScope parentScope,
+ SqlNode node) {
+ if (node == null) {
+ return;
+ }
+ if (node.getKind().belongsTo(SqlKind.QUERY)
+ || node.getKind() == SqlKind.MULTISET_QUERY_CONSTRUCTOR
+ || node.getKind() == SqlKind.MULTISET_VALUE_CONSTRUCTOR) {
+ registerQuery(parentScope, null, node, node, null, false);
+ } else if (node instanceof SqlCall) {
+ validateNodeFeature(node);
+ SqlCall call = (SqlCall) node;
+ for (int i = 0; i < call.operandCount(); i++) {
+ registerOperandSubQueries(parentScope, call, i);
+ }
+ } else if (node instanceof SqlNodeList) {
+ SqlNodeList list = (SqlNodeList) node;
+ for (int i = 0, count = list.size(); i < count; i++) {
+ SqlNode listNode = list.get(i);
+ if (listNode.getKind().belongsTo(SqlKind.QUERY)) {
+ listNode =
+ SqlStdOperatorTable.SCALAR_QUERY.createCall(
+ listNode.getParserPosition(),
+ listNode);
+ list.set(i, listNode);
+ }
+ registerSubQueries(parentScope, listNode);
+ }
+ } else {
+ // atomic node -- can be ignored
+ }
+ }
+
+ /**
+ * Registers any sub-queries inside a given call operand, and converts the
+ * operand to a scalar sub-query if the operator requires it.
+ *
+ * @param parentScope Parent scope
+ * @param call Call
+ * @param operandOrdinal Ordinal of operand within call
+ * @see SqlOperator#argumentMustBeScalar(int)
+ */
+ private void registerOperandSubQueries(
+ SqlValidatorScope parentScope,
+ SqlCall call,
+ int operandOrdinal) {
+ SqlNode operand = call.operand(operandOrdinal);
+ if (operand == null) {
+ return;
+ }
+ if (operand.getKind().belongsTo(SqlKind.QUERY)
+ && call.getOperator().argumentMustBeScalar(operandOrdinal)) {
+ operand =
+ SqlStdOperatorTable.SCALAR_QUERY.createCall(
+ operand.getParserPosition(),
+ operand);
+ call.setOperand(operandOrdinal, operand);
+ }
+ registerSubQueries(parentScope, operand);
+ }
+
+ public void validateIdentifier(SqlIdentifier id, SqlValidatorScope scope) {
+ final SqlQualified fqId = scope.fullyQualify(id);
+ if (expandColumnReferences) {
+ // NOTE jvs 9-Apr-2007: this doesn't cover ORDER BY, which has its
+ // own ideas about qualification.
+ id.assignNamesFrom(fqId.identifier);
+ } else {
+ Util.discard(fqId);
+ }
+ }
+
+ public void validateLiteral(SqlLiteral literal) {
+ switch (literal.getTypeName()) {
+ case DECIMAL:
+ // Decimal and long have the same precision (as 64-bit integers), so
+ // the unscaled value of a decimal must fit into a long.
+
+ // REVIEW jvs 4-Aug-2004: This should probably be calling over to
+ // the available calculator implementations to see what they
+ // support. For now use ESP instead.
+ //
+ // jhyde 2006/12/21: I think the limits should be baked into the
+ // type system, not dependent on the calculator implementation.
+ BigDecimal bd = (BigDecimal) literal.getValue();
+ BigInteger unscaled = bd.unscaledValue();
+ long longValue = unscaled.longValue();
+ if (!BigInteger.valueOf(longValue).equals(unscaled)) {
+ // overflow
+ throw newValidationError(literal,
+ RESOURCE.numberLiteralOutOfRange(bd.toString()));
+ }
+ break;
+
+ case DOUBLE:
+ validateLiteralAsDouble(literal);
+ break;
+
+ case BINARY:
+ final BitString bitString = (BitString) literal.getValue();
+ if ((bitString.getBitCount() % 8) != 0) {
+ throw newValidationError(literal, RESOURCE.binaryLiteralOdd());
+ }
+ break;
+
+ case DATE:
+ case TIME:
+ case TIMESTAMP:
+ Calendar calendar = literal.getValueAs(Calendar.class);
+ final int year = calendar.get(Calendar.YEAR);
+ final int era = calendar.get(Calendar.ERA);
+ if (year < 1 || era == GregorianCalendar.BC || year > 9999) {
+ throw newValidationError(literal,
+ RESOURCE.dateLiteralOutOfRange(literal.toString()));
+ }
+ break;
+
+ case INTERVAL_YEAR:
+ case INTERVAL_YEAR_MONTH:
+ case INTERVAL_MONTH:
+ case INTERVAL_DAY:
+ case INTERVAL_DAY_HOUR:
+ case INTERVAL_DAY_MINUTE:
+ case INTERVAL_DAY_SECOND:
+ case INTERVAL_HOUR:
+ case INTERVAL_HOUR_MINUTE:
+ case INTERVAL_HOUR_SECOND:
+ case INTERVAL_MINUTE:
+ case INTERVAL_MINUTE_SECOND:
+ case INTERVAL_SECOND:
+ if (literal instanceof SqlIntervalLiteral) {
+ SqlIntervalLiteral.IntervalValue interval =
+ (SqlIntervalLiteral.IntervalValue)
+ literal.getValue();
+ SqlIntervalQualifier intervalQualifier =
+ interval.getIntervalQualifier();
+
+ // ensure qualifier is good before attempting to validate literal
+ validateIntervalQualifier(intervalQualifier);
+ String intervalStr = interval.getIntervalLiteral();
+ // throws CalciteContextException if string is invalid
+ int[] values = intervalQualifier.evaluateIntervalLiteral(intervalStr,
+ literal.getParserPosition(), typeFactory.getTypeSystem());
+ Util.discard(values);
+ }
+ break;
+ default:
+ // default is to do nothing
+ }
+ }
+
+ private void validateLiteralAsDouble(SqlLiteral literal) {
+ BigDecimal bd = (BigDecimal) literal.getValue();
+ double d = bd.doubleValue();
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ // overflow
+ throw newValidationError(literal,
+ RESOURCE.numberLiteralOutOfRange(Util.toScientificNotation(bd)));
+ }
+
+ // REVIEW jvs 4-Aug-2004: what about underflow?
+ }
+
+ public void validateIntervalQualifier(SqlIntervalQualifier qualifier) {
+ assert qualifier != null;
+ boolean startPrecisionOutOfRange = false;
+ boolean fractionalSecondPrecisionOutOfRange = false;
+ final RelDataTypeSystem typeSystem = typeFactory.getTypeSystem();
+
+ final int startPrecision = qualifier.getStartPrecision(typeSystem);
+ final int fracPrecision =
+ qualifier.getFractionalSecondPrecision(typeSystem);
+ final int maxPrecision = typeSystem.getMaxPrecision(qualifier.typeName());
+ final int minPrecision = qualifier.typeName().getMinPrecision();
+ final int minScale = qualifier.typeName().getMinScale();
+ final int maxScale = typeSystem.getMaxScale(qualifier.typeName());
+ if (qualifier.isYearMonth()) {
+ if (startPrecision < minPrecision || startPrecision > maxPrecision) {
+ startPrecisionOutOfRange = true;
+ } else {
+ if (fracPrecision < minScale || fracPrecision > maxScale) {
+ fractionalSecondPrecisionOutOfRange = true;
+ }
+ }
+ } else {
+ if (startPrecision < minPrecision || startPrecision > maxPrecision) {
+ startPrecisionOutOfRange = true;
+ } else {
+ if (fracPrecision < minScale || fracPrecision > maxScale) {
+ fractionalSecondPrecisionOutOfRange = true;
+ }
+ }
+ }
+
+ if (startPrecisionOutOfRange) {
+ throw newValidationError(qualifier,
+ RESOURCE.intervalStartPrecisionOutOfRange(startPrecision,
+ "INTERVAL " + qualifier));
+ } else if (fractionalSecondPrecisionOutOfRange) {
+ throw newValidationError(qualifier,
+ RESOURCE.intervalFractionalSecondPrecisionOutOfRange(
+ fracPrecision,
+ "INTERVAL " + qualifier));
+ }
+ }
+
+ /**
+ * Validates the FROM clause of a query, or (recursively) a child node of
+ * the FROM clause: AS, OVER, JOIN, VALUES, or sub-query.
+ *
+ * @param node Node in FROM clause, typically a table or derived
+ * table
+ * @param targetRowType Desired row type of this expression, or
+ * {@link #unknownType} if not fussy. Must not be null.
+ * @param scope Scope
+ */
+ protected void validateFrom(
+ SqlNode node,
+ RelDataType targetRowType,
+ SqlValidatorScope scope) {
+ Objects.requireNonNull(targetRowType);
+ switch (node.getKind()) {
+ case AS:
+ validateFrom(
+ ((SqlCall) node).operand(0),
+ targetRowType,
+ scope);
+ break;
+ case VALUES:
+ validateValues((SqlCall) node, targetRowType, scope);
+ break;
+ case JOIN:
+ validateJoin((SqlJoin) node, scope);
+ break;
+ case OVER:
+ validateOver((SqlCall) node, scope);
+ break;
+ case UNNEST:
+ validateUnnest((SqlCall) node, scope, targetRowType);
+ break;
+ default:
+ validateQuery(node, scope, targetRowType);
+ break;
+ }
+
+ // Validate the namespace representation of the node, just in case the
+ // validation did not occur implicitly.
+ getNamespace(node, scope).validate(targetRowType);
+ }
+
+ protected void validateOver(SqlCall call, SqlValidatorScope scope) {
+ throw new AssertionError("OVER unexpected in this context");
+ }
+
+ protected void validateUnnest(SqlCall call, SqlValidatorScope scope, RelDataType targetRowType) {
+ for (int i = 0; i < call.operandCount(); i++) {
+ SqlNode expandedItem = expand(call.operand(i), scope);
+ call.setOperand(i, expandedItem);
+ }
+ validateQuery(call, scope, targetRowType);
+ }
+
+ private void checkRollUpInUsing(SqlIdentifier identifier,
+ SqlNode leftOrRight, SqlValidatorScope scope) {
+ SqlValidatorNamespace namespace = getNamespace(leftOrRight, scope);
+ if (namespace != null) {
+ SqlValidatorTable sqlValidatorTable = namespace.getTable();
+ if (sqlValidatorTable != null) {
+ Table table = sqlValidatorTable.unwrap(Table.class);
+ String column = Util.last(identifier.names);
+
+ if (table.isRolledUp(column)) {
+ throw newValidationError(identifier,
+ RESOURCE.rolledUpNotAllowed(column, "USING"));
+ }
+ }
+ }
+ }
+
+ protected void validateJoin(SqlJoin join, SqlValidatorScope scope) {
+ SqlNode left = join.getLeft();
+ SqlNode right = join.getRight();
+ SqlNode condition = join.getCondition();
+ boolean natural = join.isNatural();
+ final JoinType joinType = join.getJoinType();
+ final JoinConditionType conditionType = join.getConditionType();
+ final SqlValidatorScope joinScope = scopes.get(join);
+ validateFrom(left, unknownType, joinScope);
+ validateFrom(right, unknownType, joinScope);
+
+ // Validate condition.
+ switch (conditionType) {
+ case NONE:
+ Preconditions.checkArgument(condition == null);
+ break;
+ case ON:
+ Preconditions.checkArgument(condition != null);
+ SqlNode expandedCondition = expand(condition, joinScope);
+ join.setOperand(5, expandedCondition);
+ condition = join.getCondition();
+ validateWhereOrOn(joinScope, condition, "ON");
+ checkRollUp(null, join, condition, joinScope, "ON");
+ break;
+ case USING:
+ SqlNodeList list = (SqlNodeList) condition;
+
+ // Parser ensures that using clause is not empty.
+ Preconditions.checkArgument(list.size() > 0, "Empty USING clause");
+ for (SqlNode node : list) {
+ SqlIdentifier id = (SqlIdentifier) node;
+ final RelDataType leftColType = validateUsingCol(id, left);
+ final RelDataType rightColType = validateUsingCol(id, right);
+ if (!SqlTypeUtil.isComparable(leftColType, rightColType)) {
+ throw newValidationError(id,
+ RESOURCE.naturalOrUsingColumnNotCompatible(id.getSimple(),
+ leftColType.toString(), rightColType.toString()));
+ }
+ checkRollUpInUsing(id, left, scope);
+ checkRollUpInUsing(id, right, scope);
+ }
+ break;
+ default:
+ throw Util.unexpected(conditionType);
+ }
+
+ // Validate NATURAL.
+ if (natural) {
+ if (condition != null) {
+ throw newValidationError(condition,
+ RESOURCE.naturalDisallowsOnOrUsing());
+ }
+
+ // Join on fields that occur exactly once on each side. Ignore
+ // fields that occur more than once on either side.
+ final RelDataType leftRowType = getNamespace(left).getRowType();
+ final RelDataType rightRowType = getNamespace(right).getRowType();
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ List<String> naturalColumnNames =
+ SqlValidatorUtil.deriveNaturalJoinColumnList(nameMatcher,
+ leftRowType, rightRowType);
+
+ // Check compatibility of the chosen columns.
+ for (String name : naturalColumnNames) {
+ final RelDataType leftColType =
+ nameMatcher.field(leftRowType, name).getType();
+ final RelDataType rightColType =
+ nameMatcher.field(rightRowType, name).getType();
+ if (!SqlTypeUtil.isComparable(leftColType, rightColType)) {
+ throw newValidationError(join,
+ RESOURCE.naturalOrUsingColumnNotCompatible(name,
+ leftColType.toString(), rightColType.toString()));
+ }
+ }
+ }
+
+ // Which join types require/allow a ON/USING condition, or allow
+ // a NATURAL keyword?
+ switch (joinType) {
+ case LEFT_SEMI_JOIN:
+ if (!conformance.isLiberal()) {
+ throw newValidationError(join.getJoinTypeNode(),
+ RESOURCE.dialectDoesNotSupportFeature("LEFT SEMI JOIN"));
+ }
+ // fall through
+ case INNER:
+ case LEFT:
+ case RIGHT:
+ case FULL:
+ if ((condition == null) && !natural) {
+ throw newValidationError(join, RESOURCE.joinRequiresCondition());
+ }
+ break;
+ case COMMA:
+ case CROSS:
+ if (condition != null) {
+ throw newValidationError(join.getConditionTypeNode(),
+ RESOURCE.crossJoinDisallowsCondition());
+ }
+ if (natural) {
+ throw newValidationError(join.getConditionTypeNode(),
+ RESOURCE.crossJoinDisallowsCondition());
+ }
+ break;
+ default:
+ throw Util.unexpected(joinType);
+ }
+ }
+
+ /**
+ * Throws an error if there is an aggregate or windowed aggregate in the
+ * given clause.
+ *
+ * @param aggFinder Finder for the particular kind(s) of aggregate function
+ * @param node Parse tree
+ * @param clause Name of clause: "WHERE", "GROUP BY", "ON"
+ */
+ private void validateNoAggs(AggFinder aggFinder, SqlNode node,
+ String clause) {
+ final SqlCall agg = aggFinder.findAgg(node);
+ if (agg == null) {
+ return;
+ }
+ final SqlOperator op = agg.getOperator();
+ if (op == SqlStdOperatorTable.OVER) {
+ throw newValidationError(agg,
+ RESOURCE.windowedAggregateIllegalInClause(clause));
+ } else if (op.isGroup() || op.isGroupAuxiliary()) {
+ throw newValidationError(agg,
+ RESOURCE.groupFunctionMustAppearInGroupByClause(op.getName()));
+ } else {
+ throw newValidationError(agg,
+ RESOURCE.aggregateIllegalInClause(clause));
+ }
+ }
+
+ private RelDataType validateUsingCol(SqlIdentifier id, SqlNode leftOrRight) {
+ if (id.names.size() == 1) {
+ String name = id.names.get(0);
+ final SqlValidatorNamespace namespace = getNamespace(leftOrRight);
+ final RelDataType rowType = namespace.getRowType();
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ final RelDataTypeField field = nameMatcher.field(rowType, name);
+ if (field != null) {
+ if (nameMatcher.frequency(rowType.getFieldNames(), name) > 1) {
+ throw newValidationError(id,
+ RESOURCE.columnInUsingNotUnique(id.toString()));
+ }
+ return field.getType();
+ }
+ }
+ throw newValidationError(id, RESOURCE.columnNotFound(id.toString()));
+ }
+
+ /**
+ * Validates a SELECT statement.
+ *
+ * @param select Select statement
+ * @param targetRowType Desired row type, must not be null, may be the data
+ * type 'unknown'.
+ */
+ protected void validateSelect(
+ SqlSelect select,
+ RelDataType targetRowType) {
+ assert targetRowType != null;
+ // Namespace is either a select namespace or a wrapper around one.
+ final SelectNamespace ns =
+ getNamespace(select).unwrap(SelectNamespace.class);
+
+ // Its rowtype is null, meaning it hasn't been validated yet.
+ // This is important, because we need to take the targetRowType into
+ // account.
+ assert ns.rowType == null;
+
+ if (select.isDistinct()) {
+ validateFeature(RESOURCE.sQLFeature_E051_01(),
+ select.getModifierNode(SqlSelectKeyword.DISTINCT)
+ .getParserPosition());
+ }
+
+ final SqlNodeList selectItems = select.getSelectList();
+ RelDataType fromType = unknownType;
+ if (selectItems.size() == 1) {
+ final SqlNode selectItem = selectItems.get(0);
+ if (selectItem instanceof SqlIdentifier) {
+ SqlIdentifier id = (SqlIdentifier) selectItem;
+ if (id.isStar() && (id.names.size() == 1)) {
+ // Special case: for INSERT ... VALUES(?,?), the SQL
+ // standard says we're supposed to propagate the target
+ // types down. So iff the select list is an unqualified
+ // star (as it will be after an INSERT ... VALUES has been
+ // expanded), then propagate.
+ fromType = targetRowType;
+ }
+ }
+ }
+
+ // Make sure that items in FROM clause have distinct aliases.
+ final SelectScope fromScope = (SelectScope) getFromScope(select);
+ List<String> names = fromScope.getChildNames();
+ if (!catalogReader.nameMatcher().isCaseSensitive()) {
+ names = Lists.transform(names, s -> s.toUpperCase(Locale.ROOT));
+ }
+ final int duplicateAliasOrdinal = Util.firstDuplicate(names);
+ if (duplicateAliasOrdinal >= 0) {
+ final ScopeChild child =
+ fromScope.children.get(duplicateAliasOrdinal);
+ throw newValidationError(child.namespace.getEnclosingNode(),
+ RESOURCE.fromAliasDuplicate(child.name));
+ }
+
+ if (select.getFrom() == null) {
+ if (conformance.isFromRequired()) {
+ throw newValidationError(select, RESOURCE.selectMissingFrom());
+ }
+ } else {
+ validateFrom(select.getFrom(), fromType, fromScope);
+ }
+
+ validateWhereClause(select);
+ validateGroupClause(select);
+ validateHavingClause(select);
+ validateWindowClause(select);
+ handleOffsetFetch(select.getOffset(), select.getFetch());
+
+ // Validate the SELECT clause late, because a select item might
+ // depend on the GROUP BY list, or the window function might reference
+ // window name in the WINDOW clause etc.
+ final RelDataType rowType =
+ validateSelectList(selectItems, select, targetRowType);
+ ns.setType(rowType);
+
+ // Validate ORDER BY after we have set ns.rowType because in some
+ // dialects you can refer to columns of the select list, e.g.
+ // "SELECT empno AS x FROM emp ORDER BY x"
+ validateOrderList(select);
+
+ if (shouldCheckForRollUp(select.getFrom())) {
+ checkRollUpInSelectList(select);
+ checkRollUp(null, select, select.getWhere(), getWhereScope(select));
+ checkRollUp(null, select, select.getHaving(), getHavingScope(select));
+ checkRollUpInWindowDecl(select);
+ checkRollUpInGroupBy(select);
+ checkRollUpInOrderBy(select);
+ }
+ }
+
+ private void checkRollUpInSelectList(SqlSelect select) {
+ SqlValidatorScope scope = getSelectScope(select);
+ for (SqlNode item : select.getSelectList()) {
+ checkRollUp(null, select, item, scope);
+ }
+ }
+
+ private void checkRollUpInGroupBy(SqlSelect select) {
+ SqlNodeList group = select.getGroup();
+ if (group != null) {
+ for (SqlNode node : group) {
+ checkRollUp(null, select, node, getGroupScope(select), "GROUP BY");
+ }
+ }
+ }
+
+ private void checkRollUpInOrderBy(SqlSelect select) {
+ SqlNodeList orderList = select.getOrderList();
+ if (orderList != null) {
+ for (SqlNode node : orderList) {
+ checkRollUp(null, select, node, getOrderScope(select), "ORDER BY");
+ }
+ }
+ }
+
+ private void checkRollUpInWindow(SqlWindow window, SqlValidatorScope scope) {
+ if (window != null) {
+ for (SqlNode node : window.getPartitionList()) {
+ checkRollUp(null, window, node, scope, "PARTITION BY");
+ }
+
+ for (SqlNode node : window.getOrderList()) {
+ checkRollUp(null, window, node, scope, "ORDER BY");
+ }
+ }
+ }
+
+ private void checkRollUpInWindowDecl(SqlSelect select) {
+ for (SqlNode decl : select.getWindowList()) {
+ checkRollUpInWindow((SqlWindow) decl, getSelectScope(select));
+ }
+ }
+
+ private SqlNode stripDot(SqlNode node) {
+ if (node != null && node.getKind() == SqlKind.DOT) {
+ return stripDot(((SqlCall) node).operand(0));
+ }
+ return node;
+ }
+
+ private void checkRollUp(SqlNode grandParent, SqlNode parent,
+ SqlNode current, SqlValidatorScope scope, String optionalClause) {
+ current = stripAs(current);
+ if (current instanceof SqlCall && !(current instanceof SqlSelect)) {
+ // Validate OVER separately
+ checkRollUpInWindow(getWindowInOver(current), scope);
+ current = stripOver(current);
+
+ List<SqlNode> children = ((SqlCall) stripDot(current)).getOperandList();
+ for (SqlNode child : children) {
+ checkRollUp(parent, current, child, scope, optionalClause);
+ }
+ } else if (current instanceof SqlIdentifier) {
+ SqlIdentifier id = (SqlIdentifier) current;
+ if (!id.isStar() && isRolledUpColumn(id, scope)) {
+ if (!isAggregation(parent.getKind())
+ || !isRolledUpColumnAllowedInAgg(id, scope, (SqlCall) parent, grandParent)) {
+ String context = optionalClause != null ? optionalClause : parent.getKind().toString();
+ throw newValidationError(id,
+ RESOURCE.rolledUpNotAllowed(deriveAlias(id, 0), context));
+ }
+ }
+ }
+ }
+
+ private void checkRollUp(SqlNode grandParent, SqlNode parent,
+ SqlNode current, SqlValidatorScope scope) {
+ checkRollUp(grandParent, parent, current, scope, null);
+ }
+
+ private SqlWindow getWindowInOver(SqlNode over) {
+ if (over.getKind() == SqlKind.OVER) {
+ SqlNode window = ((SqlCall) over).getOperandList().get(1);
+ if (window instanceof SqlWindow) {
+ return (SqlWindow) window;
+ }
+ // SqlIdentifier, gets validated elsewhere
+ return null;
+ }
+ return null;
+ }
+
+ private static SqlNode stripOver(SqlNode node) {
+ switch (node.getKind()) {
+ case OVER:
+ return ((SqlCall) node).getOperandList().get(0);
+ default:
+ return node;
+ }
+ }
+
+ private Pair<String, String> findTableColumnPair(SqlIdentifier identifier,
+ SqlValidatorScope scope) {
+ final SqlCall call = makeNullaryCall(identifier);
+ if (call != null) {
+ return null;
+ }
+ SqlQualified qualified = scope.fullyQualify(identifier);
+ List<String> names = qualified.identifier.names;
+
+ if (names.size() < 2) {
+ return null;
+ }
+
+ return new Pair<>(names.get(names.size() - 2), Util.last(names));
+ }
+
+ // Returns true iff the given column is valid inside the given aggCall.
+ private boolean isRolledUpColumnAllowedInAgg(SqlIdentifier identifier, SqlValidatorScope scope,
+ SqlCall aggCall, SqlNode parent) {
+ Pair<String, String> pair = findTableColumnPair(identifier, scope);
+
+ if (pair == null) {
+ return true;
+ }
+
+ String columnName = pair.right;
+
+ SqlValidatorTable sqlValidatorTable =
+ scope.fullyQualify(identifier).namespace.getTable();
+ if (sqlValidatorTable != null) {
+ Table table = sqlValidatorTable.unwrap(Table.class);
+ return table.rolledUpColumnValidInsideAgg(columnName, aggCall, parent,
+ catalogReader.getConfig());
+ }
+ return true;
+ }
+
+
+ // Returns true iff the given column is actually rolled up.
+ private boolean isRolledUpColumn(SqlIdentifier identifier, SqlValidatorScope scope) {
+ Pair<String, String> pair = findTableColumnPair(identifier, scope);
+
+ if (pair == null) {
+ return false;
+ }
+
+ String columnName = pair.right;
+
+ SqlValidatorTable sqlValidatorTable =
+ scope.fullyQualify(identifier).namespace.getTable();
+ if (sqlValidatorTable != null) {
+ Table table = sqlValidatorTable.unwrap(Table.class);
+ return table.isRolledUp(columnName);
+ }
+ return false;
+ }
+
+ private boolean shouldCheckForRollUp(SqlNode from) {
+ if (from != null) {
+ SqlKind kind = stripAs(from).getKind();
+ return kind != SqlKind.VALUES && kind != SqlKind.SELECT;
+ }
+ return false;
+ }
+
+ /** Validates that a query can deliver the modality it promises. Only called
+ * on the top-most SELECT or set operator in the tree. */
+ private void validateModality(SqlNode query) {
+ final SqlModality modality = deduceModality(query);
+ if (query instanceof SqlSelect) {
+ final SqlSelect select = (SqlSelect) query;
+ validateModality(select, modality, true);
+ } else if (query.getKind() == SqlKind.VALUES) {
+ switch (modality) {
+ case STREAM:
+ throw newValidationError(query, Static.RESOURCE.cannotStreamValues());
+ }
+ } else {
+ assert query.isA(SqlKind.SET_QUERY);
+ final SqlCall call = (SqlCall) query;
+ for (SqlNode operand : call.getOperandList()) {
+ if (deduceModality(operand) != modality) {
+ throw newValidationError(operand,
+ Static.RESOURCE.streamSetOpInconsistentInputs());
+ }
+ validateModality(operand);
+ }
+ }
+ }
+
+ /** Return the intended modality of a SELECT or set-op. */
+ private SqlModality deduceModality(SqlNode query) {
+ if (query instanceof SqlSelect) {
+ SqlSelect select = (SqlSelect) query;
+ return select.getModifierNode(SqlSelectKeyword.STREAM) != null
+ ? SqlModality.STREAM
+ : SqlModality.RELATION;
+ } else if (query.getKind() == SqlKind.VALUES) {
+ return SqlModality.RELATION;
+ } else {
+ assert query.isA(SqlKind.SET_QUERY);
+ final SqlCall call = (SqlCall) query;
+ return deduceModality(call.getOperandList().get(0));
+ }
+ }
+
+ public boolean validateModality(SqlSelect select, SqlModality modality,
+ boolean fail) {
+ final SelectScope scope = getRawSelectScope(select);
+
+ switch (modality) {
+ case STREAM:
+ if (scope.children.size() == 1) {
+ for (ScopeChild child : scope.children) {
+ if (!child.namespace.supportsModality(modality)) {
+ if (fail) {
+ throw newValidationError(child.namespace.getNode(),
+ Static.RESOURCE.cannotConvertToStream(child.name));
+ } else {
+ return false;
+ }
+ }
+ }
+ } else {
+ int supportsModalityCount = 0;
+ for (ScopeChild child : scope.children) {
+ if (child.namespace.supportsModality(modality)) {
+ ++supportsModalityCount;
+ }
+ }
+
+ if (supportsModalityCount == 0) {
+ if (fail) {
+ String inputs = String.join(", ", scope.getChildNames());
+ throw newValidationError(select,
+ Static.RESOURCE.cannotStreamResultsForNonStreamingInputs(inputs));
+ } else {
+ return false;
+ }
+ }
+ }
+ break;
+ default:
+ for (ScopeChild child : scope.children) {
+ if (!child.namespace.supportsModality(modality)) {
+ if (fail) {
+ throw newValidationError(child.namespace.getNode(),
+ Static.RESOURCE.cannotConvertToRelation(child.name));
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Make sure that aggregation is possible.
+ final SqlNode aggregateNode = getAggregate(select);
+ if (aggregateNode != null) {
+ switch (modality) {
+ case STREAM:
+ SqlNodeList groupList = select.getGroup();
+ if (groupList == null
+ || !SqlValidatorUtil.containsMonotonic(scope, groupList)) {
+ if (fail) {
+ throw newValidationError(aggregateNode,
+ Static.RESOURCE.streamMustGroupByMonotonic());
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Make sure that ORDER BY is possible.
+ final SqlNodeList orderList = select.getOrderList();
+ if (orderList != null && orderList.size() > 0) {
+ switch (modality) {
+ case STREAM:
+ if (!hasSortedPrefix(scope, orderList)) {
+ if (fail) {
+ throw newValidationError(orderList.get(0),
+ Static.RESOURCE.streamMustOrderByMonotonic());
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /** Returns whether the prefix is sorted. */
+ private boolean hasSortedPrefix(SelectScope scope, SqlNodeList orderList) {
+ return isSortCompatible(scope, orderList.get(0), false);
+ }
+
+ private boolean isSortCompatible(SelectScope scope, SqlNode node,
+ boolean descending) {
+ switch (node.getKind()) {
+ case DESCENDING:
+ return isSortCompatible(scope, ((SqlCall) node).getOperandList().get(0),
+ true);
+ }
+ final SqlMonotonicity monotonicity = scope.getMonotonicity(node);
+ switch (monotonicity) {
+ case INCREASING:
+ case STRICTLY_INCREASING:
+ return !descending;
+ case DECREASING:
+ case STRICTLY_DECREASING:
+ return descending;
+ default:
+ return false;
+ }
+ }
+
+ protected void validateWindowClause(SqlSelect select) {
+ final SqlNodeList windowList = select.getWindowList();
+ @SuppressWarnings("unchecked") final List<SqlWindow> windows =
+ (List) windowList.getList();
+ if (windows.isEmpty()) {
+ return;
+ }
+
+ final SelectScope windowScope = (SelectScope) getFromScope(select);
+ assert windowScope != null;
+
+ // 1. ensure window names are simple
+ // 2. ensure they are unique within this scope
+ for (SqlWindow window : windows) {
+ SqlIdentifier declName = window.getDeclName();
+ if (!declName.isSimple()) {
+ throw newValidationError(declName, RESOURCE.windowNameMustBeSimple());
+ }
+
+ if (windowScope.existingWindowName(declName.toString())) {
+ throw newValidationError(declName, RESOURCE.duplicateWindowName());
+ } else {
+ windowScope.addWindowName(declName.toString());
+ }
+ }
+
+ // 7.10 rule 2
+ // Check for pairs of windows which are equivalent.
+ for (int i = 0; i < windows.size(); i++) {
+ SqlNode window1 = windows.get(i);
+ for (int j = i + 1; j < windows.size(); j++) {
+ SqlNode window2 = windows.get(j);
+ if (window1.equalsDeep(window2, Litmus.IGNORE)) {
+ throw newValidationError(window2, RESOURCE.dupWindowSpec());
+ }
+ }
+ }
+
+ for (SqlWindow window : windows) {
+ final SqlNodeList expandedOrderList =
+ (SqlNodeList) expand(window.getOrderList(), windowScope);
+ window.setOrderList(expandedOrderList);
+ expandedOrderList.validate(this, windowScope);
+
+ final SqlNodeList expandedPartitionList =
+ (SqlNodeList) expand(window.getPartitionList(), windowScope);
+ window.setPartitionList(expandedPartitionList);
+ expandedPartitionList.validate(this, windowScope);
+ }
+
+ // Hand off to validate window spec components
+ windowList.validate(this, windowScope);
+ }
+
+ public void validateWith(SqlWith with, SqlValidatorScope scope) {
+ final SqlValidatorNamespace namespace = getNamespace(with);
+ validateNamespace(namespace, unknownType);
+ }
+
+ public void validateWithItem(SqlWithItem withItem) {
+ if (withItem.columnList != null) {
+ final RelDataType rowType = getValidatedNodeType(withItem.query);
+ final int fieldCount = rowType.getFieldCount();
+ if (withItem.columnList.size() != fieldCount) {
+ throw newValidationError(withItem.columnList,
+ RESOURCE.columnCountMismatch());
+ }
+ SqlValidatorUtil.checkIdentifierListForDuplicates(
+ withItem.columnList.getList(), validationErrorFunction);
+ } else {
+ // Luckily, field names have not been make unique yet.
+ final List<String> fieldNames =
+ getValidatedNodeType(withItem.query).getFieldNames();
+ final int i = Util.firstDuplicate(fieldNames);
+ if (i >= 0) {
+ throw newValidationError(withItem.query,
+ RESOURCE.duplicateColumnAndNoColumnList(fieldNames.get(i)));
+ }
+ }
+ }
+
+ public void validateSequenceValue(SqlValidatorScope scope, SqlIdentifier id) {
+ // Resolve identifier as a table.
+ final SqlValidatorScope.ResolvedImpl resolved =
+ new SqlValidatorScope.ResolvedImpl();
+ scope.resolveTable(id.names, catalogReader.nameMatcher(),
+ SqlValidatorScope.Path.EMPTY, resolved);
+ if (resolved.count() != 1) {
+ throw newValidationError(id, RESOURCE.tableNameNotFound(id.toString()));
+ }
+ // We've found a table. But is it a sequence?
+ final SqlValidatorNamespace ns = resolved.only().namespace;
+ if (ns instanceof TableNamespace) {
+ final Table table = ns.getTable().unwrap(Table.class);
+ switch (table.getJdbcTableType()) {
+ case SEQUENCE:
+ case TEMPORARY_SEQUENCE:
+ return;
+ }
+ }
+ throw newValidationError(id, RESOURCE.notASequence(id.toString()));
+ }
+
+ public SqlValidatorScope getWithScope(SqlNode withItem) {
+ assert withItem.getKind() == SqlKind.WITH_ITEM;
+ return scopes.get(withItem);
+ }
+
+ @Override
+ public SqlValidator setEnableTypeCoercion(boolean enabled) {
+ this.enableTypeCoercion = enabled;
+ return this;
+ }
+
+ @Override
+ public boolean isTypeCoercionEnabled() {
+ return this.enableTypeCoercion;
+ }
+
+ @Override
+ public void setTypeCoercion(TypeCoercion typeCoercion) {
+ Objects.requireNonNull(typeCoercion);
+ this.typeCoercion = typeCoercion;
+ }
+
+ @Override
+ public TypeCoercion getTypeCoercion() {
+ assert isTypeCoercionEnabled();
+ return this.typeCoercion;
+ }
+
+ /**
+ * Validates the ORDER BY clause of a SELECT statement.
+ *
+ * @param select Select statement
+ */
+ protected void validateOrderList(SqlSelect select) {
+ // ORDER BY is validated in a scope where aliases in the SELECT clause
+ // are visible. For example, "SELECT empno AS x FROM emp ORDER BY x"
+ // is valid.
+ SqlNodeList orderList = select.getOrderList();
+ if (orderList == null) {
+ return;
+ }
+ if (!shouldAllowIntermediateOrderBy()) {
+ if (!cursorSet.contains(select)) {
+ throw newValidationError(select, RESOURCE.invalidOrderByPos());
+ }
+ }
+ final SqlValidatorScope orderScope = getOrderScope(select);
+ Objects.requireNonNull(orderScope);
+
+ List<SqlNode> expandList = new ArrayList<>();
+ for (SqlNode orderItem : orderList) {
+ SqlNode expandedOrderItem = expand(orderItem, orderScope);
+ expandList.add(expandedOrderItem);
+ }
+
+ SqlNodeList expandedOrderList = new SqlNodeList(
+ expandList,
+ orderList.getParserPosition());
+ select.setOrderBy(expandedOrderList);
+
+ for (SqlNode orderItem : expandedOrderList) {
+ validateOrderItem(select, orderItem);
+ }
+ }
+
+ /**
+ * Validates an item in the GROUP BY clause of a SELECT statement.
+ *
+ * @param select Select statement
+ * @param groupByItem GROUP BY clause item
+ */
+ private void validateGroupByItem(SqlSelect select, SqlNode groupByItem) {
+ final SqlValidatorScope groupByScope = getGroupScope(select);
+ groupByScope.validateExpr(groupByItem);
+ }
+
+ /**
+ * Validates an item in the ORDER BY clause of a SELECT statement.
+ *
+ * @param select Select statement
+ * @param orderItem ORDER BY clause item
+ */
+ private void validateOrderItem(SqlSelect select, SqlNode orderItem) {
+ switch (orderItem.getKind()) {
+ case DESCENDING:
+ validateFeature(RESOURCE.sQLConformance_OrderByDesc(),
+ orderItem.getParserPosition());
+ validateOrderItem(select,
+ ((SqlCall) orderItem).operand(0));
+ return;
+ }
+
+ final SqlValidatorScope orderScope = getOrderScope(select);
+ validateExpr(orderItem, orderScope);
+ }
+
+ public SqlNode expandOrderExpr(SqlSelect select, SqlNode orderExpr) {
+ final SqlNode newSqlNode =
+ new OrderExpressionExpander(select, orderExpr).go();
+ if (newSqlNode != orderExpr) {
+ final SqlValidatorScope scope = getOrderScope(select);
+ inferUnknownTypes(unknownType, scope, newSqlNode);
+ final RelDataType type = deriveType(scope, newSqlNode);
+ setValidatedNodeType(newSqlNode, type);
+ }
+ return newSqlNode;
+ }
+
+ /**
+ * Validates the GROUP BY clause of a SELECT statement. This method is
+ * called even if no GROUP BY clause is present.
+ */
+ protected void validateGroupClause(SqlSelect select) {
+ SqlNodeList groupList = select.getGroup();
+ if (groupList == null) {
+ return;
+ }
+ final String clause = "GROUP BY";
+ validateNoAggs(aggOrOverFinder, groupList, clause);
+ final SqlValidatorScope groupScope = getGroupScope(select);
+ inferUnknownTypes(unknownType, groupScope, groupList);
+
+ // expand the expression in group list.
+ List<SqlNode> expandedList = new ArrayList<>();
+ for (SqlNode groupItem : groupList) {
+ SqlNode expandedItem = expandGroupByOrHavingExpr(groupItem, groupScope, select, false);
+ expandedList.add(expandedItem);
+ }
+ groupList = new SqlNodeList(expandedList, groupList.getParserPosition());
+ select.setGroupBy(groupList);
+ for (SqlNode groupItem : expandedList) {
+ validateGroupByItem(select, groupItem);
+ }
+
+ // Nodes in the GROUP BY clause are expressions except if they are calls
+ // to the GROUPING SETS, ROLLUP or CUBE operators; this operators are not
+ // expressions, because they do not have a type.
+ for (SqlNode node : groupList) {
+ switch (node.getKind()) {
+ case GROUPING_SETS:
+ case ROLLUP:
+ case CUBE:
+ node.validate(this, groupScope);
+ break;
+ default:
+ node.validateExpr(this, groupScope);
+ }
+ }
+
+ // Derive the type of each GROUP BY item. We don't need the type, but
+ // it resolves functions, and that is necessary for deducing
+ // monotonicity.
+ final SqlValidatorScope selectScope = getSelectScope(select);
+ AggregatingSelectScope aggregatingScope = null;
+ if (selectScope instanceof AggregatingSelectScope) {
+ aggregatingScope = (AggregatingSelectScope) selectScope;
+ }
+ for (SqlNode groupItem : groupList) {
+ if (groupItem instanceof SqlNodeList
+ && ((SqlNodeList) groupItem).size() == 0) {
+ continue;
+ }
+ validateGroupItem(groupScope, aggregatingScope, groupItem);
+ }
+
+ SqlNode agg = aggFinder.findAgg(groupList);
+ if (agg != null) {
+ throw newValidationError(agg, RESOURCE.aggregateIllegalInClause(clause));
+ }
+ }
+
+ private void validateGroupItem(SqlValidatorScope groupScope,
+ AggregatingSelectScope aggregatingScope,
+ SqlNode groupItem) {
+ switch (groupItem.getKind()) {
+ case GROUPING_SETS:
+ case ROLLUP:
+ case CUBE:
+ validateGroupingSets(groupScope, aggregatingScope, (SqlCall) groupItem);
+ break;
+ default:
+ if (groupItem instanceof SqlNodeList) {
+ break;
+ }
+ final RelDataType type = deriveType(groupScope, groupItem);
+ setValidatedNodeType(groupItem, type);
+ }
+ }
+
+ private void validateGroupingSets(SqlValidatorScope groupScope,
+ AggregatingSelectScope aggregatingScope, SqlCall groupItem) {
+ for (SqlNode node : groupItem.getOperandList()) {
+ validateGroupItem(groupScope, aggregatingScope, node);
+ }
+ }
+
+ protected void validateWhereClause(SqlSelect select) {
+ // validate WHERE clause
+ final SqlNode where = select.getWhere();
+ if (where == null) {
+ return;
+ }
+ final SqlValidatorScope whereScope = getWhereScope(select);
+ final SqlNode expandedWhere = expand(where, whereScope);
+ select.setWhere(expandedWhere);
+ validateWhereOrOn(whereScope, expandedWhere, "WHERE");
+ }
+
+ protected void validateWhereOrOn(
+ SqlValidatorScope scope,
+ SqlNode condition,
+ String clause) {
+ validateNoAggs(aggOrOverOrGroupFinder, condition, clause);
+ inferUnknownTypes(
+ booleanType,
+ scope,
+ condition);
+ condition.validate(this, scope);
+
+ final RelDataType type = deriveType(scope, condition);
+ if (!SqlTypeUtil.inBooleanFamily(type)) {
+ throw newValidationError(condition, RESOURCE.condMustBeBoolean(clause));
+ }
+ }
+
+ protected void validateHavingClause(SqlSelect select) {
+ // HAVING is validated in the scope after groups have been created.
+ // For example, in "SELECT empno FROM emp WHERE empno = 10 GROUP BY
+ // deptno HAVING empno = 10", the reference to 'empno' in the HAVING
+ // clause is illegal.
+ SqlNode having = select.getHaving();
+ if (having == null) {
+ return;
+ }
+ final AggregatingScope havingScope =
+ (AggregatingScope) getSelectScope(select);
+ if (getConformance().isHavingAlias()) {
+ SqlNode newExpr = expandGroupByOrHavingExpr(having, havingScope, select, true);
+ if (having != newExpr) {
+ having = newExpr;
+ select.setHaving(newExpr);
+ }
+ }
+ havingScope.checkAggregateExpr(having, true);
+ inferUnknownTypes(
+ booleanType,
+ havingScope,
+ having);
+ having.validate(this, havingScope);
+ final RelDataType type = deriveType(havingScope, having);
+ if (!SqlTypeUtil.inBooleanFamily(type)) {
+ throw newValidationError(having, RESOURCE.havingMustBeBoolean());
+ }
+ }
+
+ protected RelDataType validateSelectList(
+ final SqlNodeList selectItems,
+ SqlSelect select,
+ RelDataType targetRowType) {
+ // First pass, ensure that aliases are unique. "*" and "TABLE.*" items
+ // are ignored.
+
+ // Validate SELECT list. Expand terms of the form "*" or "TABLE.*".
+ final SqlValidatorScope selectScope = getSelectScope(select);
+ final List<SqlNode> expandedSelectItems = new ArrayList<>();
+ final Set<String> aliases = new HashSet<>();
+ final List<Map.Entry<String, RelDataType>> fieldList = new ArrayList<>();
+
+ for (int i = 0; i < selectItems.size(); i++) {
+ SqlNode selectItem = selectItems.get(i);
+ if (selectItem instanceof SqlSelect) {
+ handleScalarSubQuery(
+ select,
+ (SqlSelect) selectItem,
+ expandedSelectItems,
+ aliases,
+ fieldList);
+ } else {
+ expandSelectItem(
+ selectItem,
+ select,
+ targetRowType.isStruct()
+ && targetRowType.getFieldCount() >= i
+ ? targetRowType.getFieldList().get(i).getType()
+ : unknownType,
+ expandedSelectItems,
+ aliases,
+ fieldList,
+ false);
+ }
+ }
+
+ // Create the new select list with expanded items. Pass through
+ // the original parser position so that any overall failures can
+ // still reference the original input text.
+ SqlNodeList newSelectList =
+ new SqlNodeList(
+ expandedSelectItems,
+ selectItems.getParserPosition());
+ if (shouldExpandIdentifiers()) {
+ select.setSelectList(newSelectList);
+ }
+ getRawSelectScope(select).setExpandedSelectList(expandedSelectItems);
+
+ // TODO: when SELECT appears as a value sub-query, should be using
+ // something other than unknownType for targetRowType
+ inferUnknownTypes(targetRowType, selectScope, newSelectList);
+
+ for (SqlNode selectItem : expandedSelectItems) {
+ validateNoAggs(groupFinder, selectItem, "SELECT");
+ validateExpr(selectItem, selectScope);
+ }
+
+ return typeFactory.createStructType(fieldList);
+ }
+
+ /**
+ * Validates an expression.
+ *
+ * @param expr Expression
+ * @param scope Scope in which expression occurs
+ */
+ private void validateExpr(SqlNode expr, SqlValidatorScope scope) {
+ if (expr instanceof SqlCall) {
+ final SqlOperator op = ((SqlCall) expr).getOperator();
+ if (op.isAggregator() && op.requiresOver()) {
+ throw newValidationError(expr,
+ RESOURCE.absentOverClause());
+ }
+ }
+
+ // Call on the expression to validate itself.
+ expr.validateExpr(this, scope);
+
+ // Perform any validation specific to the scope. For example, an
+ // aggregating scope requires that expressions are valid aggregations.
+ scope.validateExpr(expr);
+ }
+
+ /**
+ * Processes SubQuery found in Select list. Checks that is actually Scalar
+ * sub-query and makes proper entries in each of the 3 lists used to create
+ * the final rowType entry.
+ *
+ * @param parentSelect base SqlSelect item
+ * @param selectItem child SqlSelect from select list
+ * @param expandedSelectItems Select items after processing
+ * @param aliasList built from user or system values
+ * @param fieldList Built up entries for each select list entry
+ */
+ private void handleScalarSubQuery(
+ SqlSelect parentSelect,
+ SqlSelect selectItem,
+ List<SqlNode> expandedSelectItems,
+ Set<String> aliasList,
+ List<Map.Entry<String, RelDataType>> fieldList) {
+ // A scalar sub-query only has one output column.
+ if (1 != selectItem.getSelectList().size()) {
+ throw newValidationError(selectItem,
+ RESOURCE.onlyScalarSubQueryAllowed());
+ }
+
+ // No expansion in this routine just append to list.
+ expandedSelectItems.add(selectItem);
+
+ // Get or generate alias and add to list.
+ final String alias =
+ deriveAlias(
+ selectItem,
+ aliasList.size());
+ aliasList.add(alias);
+
+ final SelectScope scope = (SelectScope) getWhereScope(parentSelect);
+ final RelDataType type = deriveType(scope, selectItem);
+ setValidatedNodeType(selectItem, type);
+
+ // we do not want to pass on the RelRecordType returned
+ // by the sub query. Just the type of the single expression
+ // in the sub-query select list.
+ assert type instanceof RelRecordType;
+ RelRecordType rec = (RelRecordType) type;
+
+ RelDataType nodeType = rec.getFieldList().get(0).getType();
+ nodeType = typeFactory.createTypeWithNullability(nodeType, true);
+ fieldList.add(Pair.of(alias, nodeType));
+ }
+
+ /**
+ * Derives a row-type for INSERT and UPDATE operations.
+ *
+ * @param table Target table for INSERT/UPDATE
+ * @param targetColumnList List of target columns, or null if not specified
+ * @param append Whether to append fields to those in <code>
+ * baseRowType</code>
+ * @return Rowtype
+ */
+ protected RelDataType createTargetRowType(
+ SqlValidatorTable table,
+ SqlNodeList targetColumnList,
+ boolean append) {
+ RelDataType baseRowType = table.getRowType();
+ if (targetColumnList == null) {
+ return baseRowType;
+ }
+ List<RelDataTypeField> targetFields = baseRowType.getFieldList();
+ final List<Map.Entry<String, RelDataType>> fields = new ArrayList<>();
+ if (append) {
+ for (RelDataTypeField targetField : targetFields) {
+ fields.add(
+ Pair.of(SqlUtil.deriveAliasFromOrdinal(fields.size()),
+ targetField.getType()));
+ }
+ }
+ final Set<Integer> assignedFields = new HashSet<>();
+ final RelOptTable relOptTable = table instanceof RelOptTable
+ ? ((RelOptTable) table) : null;
+ for (SqlNode node : targetColumnList) {
+ SqlIdentifier id = (SqlIdentifier) node;
+ RelDataTypeField targetField =
+ SqlValidatorUtil.getTargetField(
+ baseRowType, typeFactory, id, catalogReader, relOptTable);
+ if (targetField == null) {
+ throw newValidationError(id,
+ RESOURCE.unknownTargetColumn(id.toString()));
+ }
+ if (!assignedFields.add(targetField.getIndex())) {
+ throw newValidationError(id,
+ RESOURCE.duplicateTargetColumn(targetField.getName()));
+ }
+ fields.add(targetField);
+ }
+ return typeFactory.createStructType(fields);
+ }
+
+ public void validateInsert(SqlInsert insert) {
+ final SqlValidatorNamespace targetNamespace = getNamespace(insert);
+ validateNamespace(targetNamespace, unknownType);
+ final RelOptTable relOptTable = SqlValidatorUtil.getRelOptTable(
+ targetNamespace, catalogReader.unwrap(Prepare.CatalogReader.class), null, null);
+ final SqlValidatorTable table = relOptTable == null
+ ? targetNamespace.getTable()
+ : relOptTable.unwrap(SqlValidatorTable.class);
+
+ // INSERT has an optional column name list. If present then
+ // reduce the rowtype to the columns specified. If not present
+ // then the entire target rowtype is used.
+ final RelDataType targetRowType =
+ createTargetRowType(
+ table,
+ insert.getTargetColumnList(),
+ false);
+
+ final SqlNode source = insert.getSource();
+ if (source instanceof SqlSelect) {
+ final SqlSelect sqlSelect = (SqlSelect) source;
+ validateSelect(sqlSelect, targetRowType);
+ } else {
+ final SqlValidatorScope scope = scopes.get(source);
+ validateQuery(source, scope, targetRowType);
+ }
+
+ // REVIEW jvs 4-Dec-2008: In FRG-365, this namespace row type is
+ // discarding the type inferred by inferUnknownTypes (which was invoked
+ // from validateSelect above). It would be better if that information
+ // were used here so that we never saw any untyped nulls during
+ // checkTypeAssignment.
+ final RelDataType sourceRowType = getNamespace(source).getRowType();
+ final RelDataType logicalTargetRowType =
+ getLogicalTargetRowType(targetRowType, insert);
+ setValidatedNodeType(insert, logicalTargetRowType);
+ final RelDataType logicalSourceRowType =
+ getLogicalSourceRowType(sourceRowType, insert);
+
+ checkFieldCount(insert.getTargetTable(), table, source,
+ logicalSourceRowType, logicalTargetRowType);
+
+ checkTypeAssignment(logicalSourceRowType, logicalTargetRowType, insert);
+
+ checkConstraint(table, source, logicalTargetRowType);
+
+ validateAccess(insert.getTargetTable(), table, SqlAccessEnum.INSERT);
+ }
+
+ /**
+ * Validates insert values against the constraint of a modifiable view.
+ *
+ * @param validatorTable Table that may wrap a ModifiableViewTable
+ * @param source The values being inserted
+ * @param targetRowType The target type for the view
+ */
+ private void checkConstraint(
+ SqlValidatorTable validatorTable,
+ SqlNode source,
+ RelDataType targetRowType) {
+ final ModifiableViewTable modifiableViewTable =
+ validatorTable.unwrap(ModifiableViewTable.class);
+ if (modifiableViewTable != null && source instanceof SqlCall) {
+ final Table table = modifiableViewTable.unwrap(Table.class);
+ final RelDataType tableRowType = table.getRowType(typeFactory);
+ final List<RelDataTypeField> tableFields = tableRowType.getFieldList();
+
+ // Get the mapping from column indexes of the underlying table
+ // to the target columns and view constraints.
+ final Map<Integer, RelDataTypeField> tableIndexToTargetField =
+ SqlValidatorUtil.getIndexToFieldMap(tableFields, targetRowType);
+ final Map<Integer, RexNode> projectMap =
+ RelOptUtil.getColumnConstraints(modifiableViewTable, targetRowType, typeFactory);
+
+ // Determine columns (indexed to the underlying table) that need
+ // to be validated against the view constraint.
+ final ImmutableBitSet targetColumns =
+ ImmutableBitSet.of(tableIndexToTargetField.keySet());
+ final ImmutableBitSet constrainedColumns =
+ ImmutableBitSet.of(projectMap.keySet());
+ final ImmutableBitSet constrainedTargetColumns =
+ targetColumns.intersect(constrainedColumns);
+
+ // Validate insert values against the view constraint.
+ final List<SqlNode> values = ((SqlCall) source).getOperandList();
+ for (final int colIndex : constrainedTargetColumns.asList()) {
+ final String colName = tableFields.get(colIndex).getName();
+ final RelDataTypeField targetField = tableIndexToTargetField.get(colIndex);
+ for (SqlNode row : values) {
+ final SqlCall call = (SqlCall) row;
+ final SqlNode sourceValue = call.operand(targetField.getIndex());
+ final ValidationError validationError =
+ new ValidationError(sourceValue,
+ RESOURCE.viewConstraintNotSatisfied(colName,
+ Util.last(validatorTable.getQualifiedName())));
+ RelOptUtil.validateValueAgainstConstraint(sourceValue,
+ projectMap.get(colIndex), validationError);
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates updates against the constraint of a modifiable view.
+ *
+ * @param validatorTable A {@link SqlValidatorTable} that may wrap a
+ * ModifiableViewTable
+ * @param update The UPDATE parse tree node
+ * @param targetRowType The target type
+ */
+ private void checkConstraint(
+ SqlValidatorTable validatorTable,
+ SqlUpdate update,
+ RelDataType targetRowType) {
+ final ModifiableViewTable modifiableViewTable =
+ validatorTable.unwrap(ModifiableViewTable.class);
+ if (modifiableViewTable != null) {
+ final Table table = modifiableViewTable.unwrap(Table.class);
+ final RelDataType tableRowType = table.getRowType(typeFactory);
+
+ final Map<Integer, RexNode> projectMap =
+ RelOptUtil.getColumnConstraints(modifiableViewTable, targetRowType,
+ typeFactory);
+ final Map<String, Integer> nameToIndex =
+ SqlValidatorUtil.mapNameToIndex(tableRowType.getFieldList());
+
+ // Validate update values against the view constraint.
+ final List<SqlNode> targets = update.getTargetColumnList().getList();
+ final List<SqlNode> sources = update.getSourceExpressionList().getList();
+ for (final Pair<SqlNode, SqlNode> column : Pair.zip(targets, sources)) {
+ final String columnName = ((SqlIdentifier) column.left).getSimple();
+ final Integer columnIndex = nameToIndex.get(columnName);
+ if (projectMap.containsKey(columnIndex)) {
+ final RexNode columnConstraint = projectMap.get(columnIndex);
+ final ValidationError validationError =
+ new ValidationError(column.right,
+ RESOURCE.viewConstraintNotSatisfied(columnName,
+ Util.last(validatorTable.getQualifiedName())));
+ RelOptUtil.validateValueAgainstConstraint(column.right,
+ columnConstraint, validationError);
+ }
+ }
+ }
+ }
+
+ private void checkFieldCount(SqlNode node, SqlValidatorTable table,
+ SqlNode source, RelDataType logicalSourceRowType,
+ RelDataType logicalTargetRowType) {
+ final int sourceFieldCount = logicalSourceRowType.getFieldCount();
+ final int targetFieldCount = logicalTargetRowType.getFieldCount();
+ if (sourceFieldCount != targetFieldCount) {
+ throw newValidationError(node,
+ RESOURCE.unmatchInsertColumn(targetFieldCount, sourceFieldCount));
+ }
+ // Ensure that non-nullable fields are targeted.
+ final InitializerContext rexBuilder =
+ new InitializerContext() {
+ public RexBuilder getRexBuilder() {
+ return new RexBuilder(typeFactory);
+ }
+
+ public RexNode convertExpression(SqlNode e) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ final List<ColumnStrategy> strategies =
+ table.unwrap(RelOptTable.class).getColumnStrategies();
+ for (final RelDataTypeField field : table.getRowType().getFieldList()) {
+ final RelDataTypeField targetField =
+ logicalTargetRowType.getField(field.getName(), true, false);
+ switch (strategies.get(field.getIndex())) {
+ case NOT_NULLABLE:
+ assert !field.getType().isNullable();
+ if (targetField == null) {
+ throw newValidationError(node,
+ RESOURCE.columnNotNullable(field.getName()));
+ }
+ break;
+ case NULLABLE:
+ assert field.getType().isNullable();
+ break;
+ case VIRTUAL:
+ case STORED:
+ if (targetField != null
+ && !isValuesWithDefault(source, targetField.getIndex())) {
+ throw newValidationError(node,
+ RESOURCE.insertIntoAlwaysGenerated(field.getName()));
+ }
+ }
+ }
+ }
+
+ /** Returns whether a query uses {@code DEFAULT} to populate a given
+ * column. */
+ private boolean isValuesWithDefault(SqlNode source, int column) {
+ switch (source.getKind()) {
+ case VALUES:
+ for (SqlNode operand : ((SqlCall) source).getOperandList()) {
+ if (!isRowWithDefault(operand, column)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isRowWithDefault(SqlNode operand, int column) {
+ switch (operand.getKind()) {
+ case ROW:
+ final SqlCall row = (SqlCall) operand;
+ return row.getOperandList().size() >= column
+ && row.getOperandList().get(column).getKind() == SqlKind.DEFAULT;
+ }
+ return false;
+ }
+
+ protected RelDataType getLogicalTargetRowType(
+ RelDataType targetRowType,
+ SqlInsert insert) {
+ if (insert.getTargetColumnList() == null
+ && conformance.isInsertSubsetColumnsAllowed()) {
+ // Target an implicit subset of columns.
+ final SqlNode source = insert.getSource();
+ final RelDataType sourceRowType = getNamespace(source).getRowType();
+ final RelDataType logicalSourceRowType =
+ getLogicalSourceRowType(sourceRowType, insert);
+ final RelDataType implicitTargetRowType =
+ typeFactory.createStructType(
+ targetRowType.getFieldList()
+ .subList(0, logicalSourceRowType.getFieldCount()));
+ final SqlValidatorNamespace targetNamespace = getNamespace(insert);
+ validateNamespace(targetNamespace, implicitTargetRowType);
+ return implicitTargetRowType;
+ } else {
+ // Either the set of columns are explicitly targeted, or target the full
+ // set of columns.
+ return targetRowType;
+ }
+ }
+
+ protected RelDataType getLogicalSourceRowType(
+ RelDataType sourceRowType,
+ SqlInsert insert) {
+ return sourceRowType;
+ }
+
+ protected void checkTypeAssignment(
+ RelDataType sourceRowType,
+ RelDataType targetRowType,
+ final SqlNode query) {
+ // NOTE jvs 23-Feb-2006: subclasses may allow for extra targets
+ // representing system-maintained columns, so stop after all sources
+ // matched
+ List<RelDataTypeField> sourceFields = sourceRowType.getFieldList();
+ List<RelDataTypeField> targetFields = targetRowType.getFieldList();
+ final int sourceCount = sourceFields.size();
+ for (int i = 0; i < sourceCount; ++i) {
+ RelDataType sourceType = sourceFields.get(i).getType();
+ RelDataType targetType = targetFields.get(i).getType();
+ if (!SqlTypeUtil.canAssignFrom(targetType, sourceType)) {
+ // FRG-255: account for UPDATE rewrite; there's
+ // probably a better way to do this.
+ int iAdjusted = i;
+ if (query instanceof SqlUpdate) {
+ int nUpdateColumns =
+ ((SqlUpdate) query).getTargetColumnList().size();
+ assert sourceFields.size() >= nUpdateColumns;
+ iAdjusted -= sourceFields.size() - nUpdateColumns;
+ }
+ SqlNode node = getNthExpr(query, iAdjusted, sourceCount);
+ String targetTypeString;
+ String sourceTypeString;
+ if (SqlTypeUtil.areCharacterSetsMismatched(
+ sourceType,
+ targetType)) {
+ sourceTypeString = sourceType.getFullTypeString();
+ targetTypeString = targetType.getFullTypeString();
+ } else {
+ sourceTypeString = sourceType.toString();
+ targetTypeString = targetType.toString();
+ }
+ throw newValidationError(node,
+ RESOURCE.typeNotAssignable(
+ targetFields.get(i).getName(), targetTypeString,
+ sourceFields.get(i).getName(), sourceTypeString));
+ }
+ }
+ }
+
+ /**
+ * Locates the n'th expression in an INSERT or UPDATE query.
+ *
+ * @param query Query
+ * @param ordinal Ordinal of expression
+ * @param sourceCount Number of expressions
+ * @return Ordinal'th expression, never null
+ */
+ private SqlNode getNthExpr(SqlNode query, int ordinal, int sourceCount) {
+ if (query instanceof SqlInsert) {
+ SqlInsert insert = (SqlInsert) query;
+ if (insert.getTargetColumnList() != null) {
+ return insert.getTargetColumnList().get(ordinal);
+ } else {
+ return getNthExpr(
+ insert.getSource(),
+ ordinal,
+ sourceCount);
+ }
+ } else if (query instanceof SqlUpdate) {
+ SqlUpdate update = (SqlUpdate) query;
+ if (update.getTargetColumnList() != null) {
+ return update.getTargetColumnList().get(ordinal);
+ } else if (update.getSourceExpressionList() != null) {
+ return update.getSourceExpressionList().get(ordinal);
+ } else {
+ return getNthExpr(
+ update.getSourceSelect(),
+ ordinal,
+ sourceCount);
+ }
+ } else if (query instanceof SqlSelect) {
+ SqlSelect select = (SqlSelect) query;
+ if (select.getSelectList().size() == sourceCount) {
+ return select.getSelectList().get(ordinal);
+ } else {
+ return query; // give up
+ }
+ } else {
+ return query; // give up
+ }
+ }
+
+ public void validateDelete(SqlDelete call) {
+ final SqlSelect sqlSelect = call.getSourceSelect();
+ validateSelect(sqlSelect, unknownType);
+
+ final SqlValidatorNamespace targetNamespace = getNamespace(call);
+ validateNamespace(targetNamespace, unknownType);
+ final SqlValidatorTable table = targetNamespace.getTable();
+
+ validateAccess(call.getTargetTable(), table, SqlAccessEnum.DELETE);
+ }
+
+ public void validateUpdate(SqlUpdate call) {
+ final SqlValidatorNamespace targetNamespace = getNamespace(call);
+ validateNamespace(targetNamespace, unknownType);
+ final RelOptTable relOptTable = SqlValidatorUtil.getRelOptTable(
+ targetNamespace, catalogReader.unwrap(Prepare.CatalogReader.class), null, null);
+ final SqlValidatorTable table = relOptTable == null
+ ? targetNamespace.getTable()
+ : relOptTable.unwrap(SqlValidatorTable.class);
+
+ final RelDataType targetRowType =
+ createTargetRowType(
+ table,
+ call.getTargetColumnList(),
+ true);
+
+ final SqlSelect select = call.getSourceSelect();
+ validateSelect(select, targetRowType);
+
+ final RelDataType sourceRowType = getNamespace(call).getRowType();
+ checkTypeAssignment(sourceRowType, targetRowType, call);
+
+ checkConstraint(table, call, targetRowType);
+
+ validateAccess(call.getTargetTable(), table, SqlAccessEnum.UPDATE);
+ }
+
+ public void validateMerge(SqlMerge call) {
+ SqlSelect sqlSelect = call.getSourceSelect();
+ // REVIEW zfong 5/25/06 - Does an actual type have to be passed into
+ // validateSelect()?
+
+ // REVIEW jvs 6-June-2006: In general, passing unknownType like
+ // this means we won't be able to correctly infer the types
+ // for dynamic parameter markers (SET x = ?). But
+ // maybe validateUpdate and validateInsert below will do
+ // the job?
+
+ // REVIEW ksecretan 15-July-2011: They didn't get a chance to
+ // since validateSelect() would bail.
+ // Let's use the update/insert targetRowType when available.
+ IdentifierNamespace targetNamespace =
+ (IdentifierNamespace) getNamespace(call.getTargetTable());
+ validateNamespace(targetNamespace, unknownType);
+
+ SqlValidatorTable table = targetNamespace.getTable();
+ validateAccess(call.getTargetTable(), table, SqlAccessEnum.UPDATE);
+
+ RelDataType targetRowType = unknownType;
+
+ if (call.getUpdateCall() != null) {
+ targetRowType = createTargetRowType(
+ table,
+ call.getUpdateCall().getTargetColumnList(),
+ true);
+ }
+ if (call.getInsertCall() != null) {
+ targetRowType = createTargetRowType(
+ table,
+ call.getInsertCall().getTargetColumnList(),
+ false);
+ }
+
+ validateSelect(sqlSelect, targetRowType);
+
+ if (call.getUpdateCall() != null) {
+ validateUpdate(call.getUpdateCall());
+ }
+ if (call.getInsertCall() != null) {
+ validateInsert(call.getInsertCall());
+ }
+ }
+
+ /**
+ * Validates access to a table.
+ *
+ * @param table Table
+ * @param requiredAccess Access requested on table
+ */
+ private void validateAccess(
+ SqlNode node,
+ SqlValidatorTable table,
+ SqlAccessEnum requiredAccess) {
+ if (table != null) {
+ SqlAccessType access = table.getAllowedAccess();
+ if (!access.allowsAccess(requiredAccess)) {
+ throw newValidationError(node,
+ RESOURCE.accessNotAllowed(requiredAccess.name(),
+ table.getQualifiedName().toString()));
+ }
+ }
+ }
+
+ /**
+ * Validates a VALUES clause.
+ *
+ * @param node Values clause
+ * @param targetRowType Row type which expression must conform to
+ * @param scope Scope within which clause occurs
+ */
+ protected void validateValues(
+ SqlCall node,
+ RelDataType targetRowType,
+ final SqlValidatorScope scope) {
+ assert node.getKind() == SqlKind.VALUES;
+
+ final List<SqlNode> operands = node.getOperandList();
+ for (SqlNode operand : operands) {
+ if (!(operand.getKind() == SqlKind.ROW)) {
+ throw Util.needToImplement(
+ "Values function where operands are scalars");
+ }
+
+ SqlCall rowConstructor = (SqlCall) operand;
+ if (conformance.isInsertSubsetColumnsAllowed() && targetRowType.isStruct()
+ && rowConstructor.operandCount() < targetRowType.getFieldCount()) {
+ targetRowType =
+ typeFactory.createStructType(
+ targetRowType.getFieldList()
+ .subList(0, rowConstructor.operandCount()));
+ } else if (targetRowType.isStruct()
+ && rowConstructor.operandCount() != targetRowType.getFieldCount()) {
+ return;
+ }
+
+ inferUnknownTypes(
+ targetRowType,
+ scope,
+ rowConstructor);
+
+ if (targetRowType.isStruct()) {
+ for (Pair<SqlNode, RelDataTypeField> pair
+ : Pair.zip(rowConstructor.getOperandList(),
+ targetRowType.getFieldList())) {
+ if (!pair.right.getType().isNullable()
+ && SqlUtil.isNullLiteral(pair.left, false)) {
+ throw newValidationError(node,
+ RESOURCE.columnNotNullable(pair.right.getName()));
+ }
+ }
+ }
+ }
+
+ for (SqlNode operand : operands) {
+ operand.validate(this, scope);
+ }
+
+ // validate that all row types have the same number of columns
+ // and that expressions in each column are compatible.
+ // A values expression is turned into something that looks like
+ // ROW(type00, type01,...), ROW(type11,...),...
+ final int rowCount = operands.size();
+ if (rowCount >= 2) {
+ SqlCall firstRow = (SqlCall) operands.get(0);
+ final int columnCount = firstRow.operandCount();
+
+ // 1. check that all rows have the same cols length
+ for (SqlNode operand : operands) {
+ SqlCall thisRow = (SqlCall) operand;
+ if (columnCount != thisRow.operandCount()) {
+ throw newValidationError(node,
+ RESOURCE.incompatibleValueType(
+ SqlStdOperatorTable.VALUES.getName()));
+ }
+ }
+
+ // 2. check if types at i:th position in each row are compatible
+ for (int col = 0; col < columnCount; col++) {
+ final int c = col;
+ final RelDataType type =
+ typeFactory.leastRestrictive(
+ new AbstractList<RelDataType>() {
+ public RelDataType get(int row) {
+ SqlCall thisRow = (SqlCall) operands.get(row);
+ return deriveType(scope, thisRow.operand(c));
+ }
+
+ public int size() {
+ return rowCount;
+ }
+ });
+
+ if (null == type) {
+ throw newValidationError(node,
+ RESOURCE.incompatibleValueType(
+ SqlStdOperatorTable.VALUES.getName()));
+ }
+ }
+ }
+ }
+
+ public void validateDataType(SqlDataTypeSpec dataType) {
+ }
+
+ public void validateDynamicParam(SqlDynamicParam dynamicParam) {
+ }
+
+ /**
+ * Throws a validator exception with access to the validator context.
+ * The exception is determined when an instance is created.
+ */
+ private class ValidationError implements Supplier<CalciteContextException> {
+ private final SqlNode sqlNode;
+ private final Resources.ExInst<SqlValidatorException> validatorException;
+
+ ValidationError(SqlNode sqlNode,
+ Resources.ExInst<SqlValidatorException> validatorException) {
+ this.sqlNode = sqlNode;
+ this.validatorException = validatorException;
+ }
+
+ public CalciteContextException get() {
+ return newValidationError(sqlNode, validatorException);
+ }
+ }
+
+ /**
+ * Throws a validator exception with access to the validator context.
+ * The exception is determined when the function is applied.
+ */
+ class ValidationErrorFunction
+ implements Function2<SqlNode, Resources.ExInst<SqlValidatorException>,
+ CalciteContextException> {
+ @Override public CalciteContextException apply(
+ SqlNode v0, Resources.ExInst<SqlValidatorException> v1) {
+ return newValidationError(v0, v1);
+ }
+ }
+
+ public ValidationErrorFunction getValidationErrorFunction() {
+ return validationErrorFunction;
+ }
+
+ public CalciteContextException newValidationError(SqlNode node,
+ Resources.ExInst<SqlValidatorException> e) {
+ assert node != null;
+ final SqlParserPos pos = node.getParserPosition();
+ return SqlUtil.newContextException(pos, e);
+ }
+
+ protected SqlWindow getWindowByName(
+ SqlIdentifier id,
+ SqlValidatorScope scope) {
+ SqlWindow window = null;
+ if (id.isSimple()) {
+ final String name = id.getSimple();
+ window = scope.lookupWindow(name);
+ }
+ if (window == null) {
+ throw newValidationError(id, RESOURCE.windowNotFound(id.toString()));
+ }
+ return window;
+ }
+
+ public SqlWindow resolveWindow(
+ SqlNode windowOrRef,
+ SqlValidatorScope scope,
+ boolean populateBounds) {
+ SqlWindow window;
+ if (windowOrRef instanceof SqlIdentifier) {
+ window = getWindowByName((SqlIdentifier) windowOrRef, scope);
+ } else {
+ window = (SqlWindow) windowOrRef;
+ }
+ while (true) {
+ final SqlIdentifier refId = window.getRefName();
+ if (refId == null) {
+ break;
+ }
+ final String refName = refId.getSimple();
+ SqlWindow refWindow = scope.lookupWindow(refName);
+ if (refWindow == null) {
+ throw newValidationError(refId, RESOURCE.windowNotFound(refName));
+ }
+ window = window.overlay(refWindow, this);
+ }
+
+ if (populateBounds) {
+ window.populateBounds();
+ }
+ return window;
+ }
+
+ public SqlNode getOriginal(SqlNode expr) {
+ SqlNode original = originalExprs.get(expr);
+ if (original == null) {
+ original = expr;
+ }
+ return original;
+ }
+
+ public void setOriginal(SqlNode expr, SqlNode original) {
+ // Don't overwrite the original original.
+ originalExprs.putIfAbsent(expr, original);
+ }
+
+ SqlValidatorNamespace lookupFieldNamespace(RelDataType rowType, String name) {
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ final RelDataTypeField field = nameMatcher.field(rowType, name);
+ if (field == null) {
+ return null;
+ }
+ return new FieldNamespace(this, field.getType());
+ }
+
+ public void validateWindow(
+ SqlNode windowOrId,
+ SqlValidatorScope scope,
+ SqlCall call) {
+ // Enable nested aggregates with window aggregates (OVER operator)
+ inWindow = true;
+
+ final SqlWindow targetWindow;
+ switch (windowOrId.getKind()) {
+ case IDENTIFIER:
+ // Just verify the window exists in this query. It will validate
+ // when the definition is processed
+ targetWindow = getWindowByName((SqlIdentifier) windowOrId, scope);
+ break;
+ case WINDOW:
+ targetWindow = (SqlWindow) windowOrId;
+ break;
+ default:
+ throw Util.unexpected(windowOrId.getKind());
+ }
+
+ assert targetWindow.getWindowCall() == null;
+ targetWindow.setWindowCall(call);
+ targetWindow.validate(this, scope);
+ targetWindow.setWindowCall(null);
+ call.validate(this, scope);
+
+ validateAggregateParams(call, null, null, scope);
+
+ // Disable nested aggregates post validation
+ inWindow = false;
+ }
+
+ @Override public void validateMatchRecognize(SqlCall call) {
+ final SqlMatchRecognize matchRecognize = (SqlMatchRecognize) call;
+ final MatchRecognizeScope scope =
+ (MatchRecognizeScope) getMatchRecognizeScope(matchRecognize);
+
+ final MatchRecognizeNamespace ns =
+ getNamespace(call).unwrap(MatchRecognizeNamespace.class);
+ assert ns.rowType == null;
+
+ // rows per match
+ final SqlLiteral rowsPerMatch = matchRecognize.getRowsPerMatch();
+ final boolean allRows = rowsPerMatch != null
+ && rowsPerMatch.getValue()
+ == SqlMatchRecognize.RowsPerMatchOption.ALL_ROWS;
+
+ final RelDataTypeFactory.Builder typeBuilder = typeFactory.builder();
+
+ // parse PARTITION BY column
+ SqlNodeList partitionBy = matchRecognize.getPartitionList();
+ if (partitionBy != null) {
+ for (SqlNode node : partitionBy) {
+ SqlIdentifier identifier = (SqlIdentifier) node;
+ identifier.validate(this, scope);
+ RelDataType type = deriveType(scope, identifier);
+ String name = identifier.names.get(1);
+ typeBuilder.add(name, type);
+ }
+ }
+
+ // parse ORDER BY column
+ SqlNodeList orderBy = matchRecognize.getOrderList();
+ if (orderBy != null) {
+ for (SqlNode node : orderBy) {
+ node.validate(this, scope);
+ SqlIdentifier identifier;
+ if (node instanceof SqlBasicCall) {
+ identifier = (SqlIdentifier) ((SqlBasicCall) node).getOperands()[0];
+ } else {
+ identifier = (SqlIdentifier) node;
+ }
+
+ if (allRows) {
+ RelDataType type = deriveType(scope, identifier);
+ String name = identifier.names.get(1);
+ if (!typeBuilder.nameExists(name)) {
+ typeBuilder.add(name, type);
+ }
+ }
+ }
+ }
+
+ if (allRows) {
+ final SqlValidatorNamespace sqlNs =
+ getNamespace(matchRecognize.getTableRef());
+ final RelDataType inputDataType = sqlNs.getRowType();
+ for (RelDataTypeField fs : inputDataType.getFieldList()) {
+ if (!typeBuilder.nameExists(fs.getName())) {
+ typeBuilder.add(fs);
+ }
+ }
+ }
+
+ // retrieve pattern variables used in pattern and subset
+ SqlNode pattern = matchRecognize.getPattern();
+ PatternVarVisitor visitor = new PatternVarVisitor(scope);
+ pattern.accept(visitor);
+
+ SqlLiteral interval = matchRecognize.getInterval();
+ if (interval != null) {
+ interval.validate(this, scope);
+ if (((SqlIntervalLiteral) interval).signum() < 0) {
+ throw newValidationError(interval,
+ RESOURCE.intervalMustBeNonNegative(interval.toValue()));
+ }
+ if (orderBy == null || orderBy.size() == 0) {
+ throw newValidationError(interval,
+ RESOURCE.cannotUseWithinWithoutOrderBy());
+ }
+
+ SqlNode firstOrderByColumn = orderBy.getList().get(0);
+ SqlIdentifier identifier;
+ if (firstOrderByColumn instanceof SqlBasicCall) {
+ identifier = (SqlIdentifier) ((SqlBasicCall) firstOrderByColumn).getOperands()[0];
+ } else {
+ identifier = (SqlIdentifier) firstOrderByColumn;
+ }
+ RelDataType firstOrderByColumnType = deriveType(scope, identifier);
+ if (firstOrderByColumnType.getSqlTypeName() != SqlTypeName.TIMESTAMP) {
+ throw newValidationError(interval,
+ RESOURCE.firstColumnOfOrderByMustBeTimestamp());
+ }
+
+ SqlNode expand = expand(interval, scope);
+ RelDataType type = deriveType(scope, expand);
+ setValidatedNodeType(interval, type);
+ }
+
+ validateDefinitions(matchRecognize, scope);
+
+ SqlNodeList subsets = matchRecognize.getSubsetList();
+ if (subsets != null && subsets.size() > 0) {
+ for (SqlNode node : subsets) {
+ List<SqlNode> operands = ((SqlCall) node).getOperandList();
+ String leftString = ((SqlIdentifier) operands.get(0)).getSimple();
+ if (scope.getPatternVars().contains(leftString)) {
+ throw newValidationError(operands.get(0),
+ RESOURCE.patternVarAlreadyDefined(leftString));
+ }
+ scope.addPatternVar(leftString);
+ for (SqlNode right : (SqlNodeList) operands.get(1)) {
+ SqlIdentifier id = (SqlIdentifier) right;
+ if (!scope.getPatternVars().contains(id.getSimple())) {
+ throw newValidationError(id,
+ RESOURCE.unknownPattern(id.getSimple()));
+ }
+ scope.addPatternVar(id.getSimple());
+ }
+ }
+ }
+
+ // validate AFTER ... SKIP TO
+ final SqlNode skipTo = matchRecognize.getAfter();
+ if (skipTo instanceof SqlCall) {
+ final SqlCall skipToCall = (SqlCall) skipTo;
+ final SqlIdentifier id = skipToCall.operand(0);
+ if (!scope.getPatternVars().contains(id.getSimple())) {
+ throw newValidationError(id,
+ RESOURCE.unknownPattern(id.getSimple()));
+ }
+ }
+
+ List<Map.Entry<String, RelDataType>> measureColumns =
+ validateMeasure(matchRecognize, scope, allRows);
+ for (Map.Entry<String, RelDataType> c : measureColumns) {
+ if (!typeBuilder.nameExists(c.getKey())) {
+ typeBuilder.add(c.getKey(), c.getValue());
+ }
+ }
+
+ final RelDataType rowType = typeBuilder.build();
+ if (matchRecognize.getMeasureList().size() == 0) {
+ ns.setType(getNamespace(matchRecognize.getTableRef()).getRowType());
+ } else {
+ ns.setType(rowType);
+ }
+ }
+
+ private List<Map.Entry<String, RelDataType>> validateMeasure(SqlMatchRecognize mr,
+ MatchRecognizeScope scope, boolean allRows) {
+ final List<String> aliases = new ArrayList<>();
+ final List<SqlNode> sqlNodes = new ArrayList<>();
+ final SqlNodeList measures = mr.getMeasureList();
+ final List<Map.Entry<String, RelDataType>> fields = new ArrayList<>();
+
+ for (SqlNode measure : measures) {
+ assert measure instanceof SqlCall;
+ final String alias = deriveAlias(measure, aliases.size());
+ aliases.add(alias);
+
+ SqlNode expand = expand(measure, scope);
+ expand = navigationInMeasure(expand, allRows);
+ setOriginal(expand, measure);
+
+ inferUnknownTypes(unknownType, scope, expand);
+ final RelDataType type = deriveType(scope, expand);
+ setValidatedNodeType(measure, type);
+
+ fields.add(Pair.of(alias, type));
+ sqlNodes.add(
+ SqlStdOperatorTable.AS.createCall(SqlParserPos.ZERO, expand,
+ new SqlIdentifier(alias, SqlParserPos.ZERO)));
+ }
+
+ SqlNodeList list = new SqlNodeList(sqlNodes, measures.getParserPosition());
+ inferUnknownTypes(unknownType, scope, list);
+
+ for (SqlNode node : list) {
+ validateExpr(node, scope);
+ }
+
+ mr.setOperand(SqlMatchRecognize.OPERAND_MEASURES, list);
+
+ return fields;
+ }
+
+ private SqlNode navigationInMeasure(SqlNode node, boolean allRows) {
+ final Set<String> prefix = node.accept(new PatternValidator(true));
+ Util.discard(prefix);
+ final List<SqlNode> ops = ((SqlCall) node).getOperandList();
+
+ final SqlOperator defaultOp =
+ allRows ? SqlStdOperatorTable.RUNNING : SqlStdOperatorTable.FINAL;
+ final SqlNode op0 = ops.get(0);
+ if (!isRunningOrFinal(op0.getKind())
+ || !allRows && op0.getKind() == SqlKind.RUNNING) {
+ SqlNode newNode = defaultOp.createCall(SqlParserPos.ZERO, op0);
+ node = SqlStdOperatorTable.AS.createCall(SqlParserPos.ZERO, newNode, ops.get(1));
+ }
+
+ node = new NavigationExpander().go(node);
+ return node;
+ }
+
+ private void validateDefinitions(SqlMatchRecognize mr,
+ MatchRecognizeScope scope) {
+ final Set<String> aliases = catalogReader.nameMatcher().createSet();
+ for (SqlNode item : mr.getPatternDefList().getList()) {
+ final String alias = alias(item);
+ if (!aliases.add(alias)) {
+ throw newValidationError(item,
+ Static.RESOURCE.patternVarAlreadyDefined(alias));
+ }
+ scope.addPatternVar(alias);
+ }
+
+ final List<SqlNode> sqlNodes = new ArrayList<>();
+ for (SqlNode item : mr.getPatternDefList().getList()) {
+ final String alias = alias(item);
+ SqlNode expand = expand(item, scope);
+ expand = navigationInDefine(expand, alias);
+ setOriginal(expand, item);
+
+ inferUnknownTypes(booleanType, scope, expand);
+ expand.validate(this, scope);
+
+ // Some extra work need required here.
+ // In PREV, NEXT, FINAL and LAST, only one pattern variable is allowed.
+ sqlNodes.add(
+ SqlStdOperatorTable.AS.createCall(SqlParserPos.ZERO, expand,
+ new SqlIdentifier(alias, SqlParserPos.ZERO)));
+
+ final RelDataType type = deriveType(scope, expand);
+ if (!SqlTypeUtil.inBooleanFamily(type)) {
+ throw newValidationError(expand, RESOURCE.condMustBeBoolean("DEFINE"));
+ }
+ setValidatedNodeType(item, type);
+ }
+
+ SqlNodeList list =
+ new SqlNodeList(sqlNodes, mr.getPatternDefList().getParserPosition());
+ inferUnknownTypes(unknownType, scope, list);
+ for (SqlNode node : list) {
+ validateExpr(node, scope);
+ }
+ mr.setOperand(SqlMatchRecognize.OPERAND_PATTERN_DEFINES, list);
+ }
+
+ /** Returns the alias of a "expr AS alias" expression. */
+ private static String alias(SqlNode item) {
+ assert item instanceof SqlCall;
+ assert item.getKind() == SqlKind.AS;
+ final SqlIdentifier identifier = ((SqlCall) item).operand(1);
+ return identifier.getSimple();
+ }
+
+ /** Checks that all pattern variables within a function are the same,
+ * and canonizes expressions such as {@code PREV(B.price)} to
+ * {@code LAST(B.price, 0)}. */
+ private SqlNode navigationInDefine(SqlNode node, String alpha) {
+ Set<String> prefix = node.accept(new PatternValidator(false));
+ Util.discard(prefix);
+ node = new NavigationExpander().go(node);
+ node = new NavigationReplacer(alpha).go(node);
+ return node;
+ }
+
+ public void validateAggregateParams(SqlCall aggCall, SqlNode filter,
+ SqlNodeList orderList, SqlValidatorScope scope) {
+ // For "agg(expr)", expr cannot itself contain aggregate function
+ // invocations. For example, "SUM(2 * MAX(x))" is illegal; when
+ // we see it, we'll report the error for the SUM (not the MAX).
+ // For more than one level of nesting, the error which results
+ // depends on the traversal order for validation.
+ //
+ // For a windowed aggregate "agg(expr)", expr can contain an aggregate
+ // function. For example,
+ // SELECT AVG(2 * MAX(x)) OVER (PARTITION BY y)
+ // FROM t
+ // GROUP BY y
+ // is legal. Only one level of nesting is allowed since non-windowed
+ // aggregates cannot nest aggregates.
+
+ // Store nesting level of each aggregate. If an aggregate is found at an invalid
+ // nesting level, throw an assert.
+ final AggFinder a;
+ if (inWindow) {
+ a = overFinder;
+ } else {
+ a = aggOrOverFinder;
+ }
+
+ for (SqlNode param : aggCall.getOperandList()) {
+ if (a.findAgg(param) != null) {
+ throw newValidationError(aggCall, RESOURCE.nestedAggIllegal());
+ }
+ }
+ if (filter != null) {
+ if (a.findAgg(filter) != null) {
+ throw newValidationError(filter, RESOURCE.aggregateInFilterIllegal());
+ }
+ }
+ if (orderList != null) {
+ for (SqlNode param : orderList) {
+ if (a.findAgg(param) != null) {
+ throw newValidationError(aggCall,
+ RESOURCE.aggregateInWithinGroupIllegal());
+ }
+ }
+ }
+
+ final SqlAggFunction op = (SqlAggFunction) aggCall.getOperator();
+ switch (op.requiresGroupOrder()) {
+ case MANDATORY:
+ if (orderList == null || orderList.size() == 0) {
+ throw newValidationError(aggCall,
+ RESOURCE.aggregateMissingWithinGroupClause(op.getName()));
+ }
+ break;
+ case OPTIONAL:
+ break;
+ case IGNORED:
+ // rewrite the order list to empty
+ if (orderList != null) {
+ orderList.getList().clear();
+ }
+ break;
+ case FORBIDDEN:
+ if (orderList != null && orderList.size() != 0) {
+ throw newValidationError(aggCall,
+ RESOURCE.withinGroupClauseIllegalInAggregate(op.getName()));
+ }
+ break;
+ default:
+ throw new AssertionError(op);
+ }
+ }
+
+ public void validateCall(
+ SqlCall call,
+ SqlValidatorScope scope) {
+ final SqlOperator operator = call.getOperator();
+ if ((call.operandCount() == 0)
+ && (operator.getSyntax() == SqlSyntax.FUNCTION_ID)
+ && !call.isExpanded()
+ && !conformance.allowNiladicParentheses()) {
+ // For example, "LOCALTIME()" is illegal. (It should be
+ // "LOCALTIME", which would have been handled as a
+ // SqlIdentifier.)
+ throw handleUnresolvedFunction(call, (SqlFunction) operator,
+ ImmutableList.of(), null);
+ }
+
+ SqlValidatorScope operandScope = scope.getOperandScope(call);
+
+ if (operator instanceof SqlFunction
+ && ((SqlFunction) operator).getFunctionType()
+ == SqlFunctionCategory.MATCH_RECOGNIZE
+ && !(operandScope instanceof MatchRecognizeScope)) {
+ throw newValidationError(call,
+ Static.RESOURCE.functionMatchRecognizeOnly(call.toString()));
+ }
+ // Delegate validation to the operator.
+ operator.validateCall(call, this, scope, operandScope);
+ }
+
+ /**
+ * Validates that a particular feature is enabled. By default, all features
+ * are enabled; subclasses may override this method to be more
+ * discriminating.
+ *
+ * @param feature feature being used, represented as a resource instance
+ * @param context parser position context for error reporting, or null if
+ */
+ protected void validateFeature(
+ Feature feature,
+ SqlParserPos context) {
+ // By default, do nothing except to verify that the resource
+ // represents a real feature definition.
+ assert feature.getProperties().get("FeatureDefinition") != null;
+ }
+
+ public SqlNode expand(SqlNode expr, SqlValidatorScope scope) {
+ final Expander expander = new Expander(this, scope);
+ SqlNode newExpr = expr.accept(expander);
+ if (expr != newExpr) {
+ setOriginal(newExpr, expr);
+ }
+ return newExpr;
+ }
+
+ public SqlNode expandGroupByOrHavingExpr(SqlNode expr,
+ SqlValidatorScope scope, SqlSelect select, boolean havingExpression) {
+ final Expander expander = new ExtendedExpander(this, scope, select, expr,
+ havingExpression);
+ SqlNode newExpr = expr.accept(expander);
+ if (expr != newExpr) {
+ setOriginal(newExpr, expr);
+ }
+ return newExpr;
+ }
+
+ public boolean isSystemField(RelDataTypeField field) {
+ return false;
+ }
+
+ public List<List<String>> getFieldOrigins(SqlNode sqlQuery) {
+ if (sqlQuery instanceof SqlExplain) {
+ return Collections.emptyList();
+ }
+ final RelDataType rowType = getValidatedNodeType(sqlQuery);
+ final int fieldCount = rowType.getFieldCount();
+ if (!sqlQuery.isA(SqlKind.QUERY)) {
+ return Collections.nCopies(fieldCount, null);
+ }
+ final List<List<String>> list = new ArrayList<>();
+ for (int i = 0; i < fieldCount; i++) {
+ list.add(getFieldOrigin(sqlQuery, i));
+ }
+ return ImmutableNullableList.copyOf(list);
+ }
+
+ private List<String> getFieldOrigin(SqlNode sqlQuery, int i) {
+ if (sqlQuery instanceof SqlSelect) {
+ SqlSelect sqlSelect = (SqlSelect) sqlQuery;
+ final SelectScope scope = getRawSelectScope(sqlSelect);
+ final List<SqlNode> selectList = scope.getExpandedSelectList();
+ final SqlNode selectItem = stripAs(selectList.get(i));
+ if (selectItem instanceof SqlIdentifier) {
+ final SqlQualified qualified =
+ scope.fullyQualify((SqlIdentifier) selectItem);
+ SqlValidatorNamespace namespace = qualified.namespace;
+ final SqlValidatorTable table = namespace.getTable();
+ if (table == null) {
+ return null;
+ }
+ final List<String> origin =
+ new ArrayList<>(table.getQualifiedName());
+ for (String name : qualified.suffix()) {
+ namespace = namespace.lookupChild(name);
+ if (namespace == null) {
+ return null;
+ }
+ origin.add(name);
+ }
+ return origin;
+ }
+ return null;
+ } else if (sqlQuery instanceof SqlOrderBy) {
+ return getFieldOrigin(((SqlOrderBy) sqlQuery).query, i);
+ } else {
+ return null;
+ }
+ }
+
+ public RelDataType getParameterRowType(SqlNode sqlQuery) {
+ // NOTE: We assume that bind variables occur in depth-first tree
+ // traversal in the same order that they occurred in the SQL text.
+ final List<RelDataType> types = new ArrayList<>();
+ // NOTE: but parameters on fetch/offset would be counted twice
+ // as they are counted in the SqlOrderBy call and the inner SqlSelect call
+ final Set<SqlNode> alreadyVisited = new HashSet<>();
+ sqlQuery.accept(
+ new SqlShuttle() {
+
+ @Override public SqlNode visit(SqlDynamicParam param) {
+ if (alreadyVisited.add(param)) {
+ RelDataType type = getValidatedNodeType(param);
+ types.add(type);
+ }
+ return param;
+ }
+ });
+ return typeFactory.createStructType(
+ types,
+ new AbstractList<String>() {
+ @Override public String get(int index) {
+ return "?" + index;
+ }
+
+ @Override public int size() {
+ return types.size();
+ }
+ });
+ }
+
+ public void validateColumnListParams(
+ SqlFunction function,
+ List<RelDataType> argTypes,
+ List<SqlNode> operands) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static boolean isPhysicalNavigation(SqlKind kind) {
+ return kind == SqlKind.PREV || kind == SqlKind.NEXT;
+ }
+
+ private static boolean isLogicalNavigation(SqlKind kind) {
+ return kind == SqlKind.FIRST || kind == SqlKind.LAST;
+ }
+
+ private static boolean isAggregation(SqlKind kind) {
+ return kind == SqlKind.SUM || kind == SqlKind.SUM0
+ || kind == SqlKind.AVG || kind == SqlKind.COUNT
+ || kind == SqlKind.MAX || kind == SqlKind.MIN;
+ }
+
+ private static boolean isRunningOrFinal(SqlKind kind) {
+ return kind == SqlKind.RUNNING || kind == SqlKind.FINAL;
+ }
+
+ private static boolean isSingleVarRequired(SqlKind kind) {
+ return isPhysicalNavigation(kind)
+ || isLogicalNavigation(kind)
+ || isAggregation(kind);
+ }
+
+ //~ Inner Classes ----------------------------------------------------------
+
+ /**
+ * Common base class for DML statement namespaces.
+ */
+ public static class DmlNamespace extends IdentifierNamespace {
+ protected DmlNamespace(SqlValidatorImpl validator, SqlNode id,
+ SqlNode enclosingNode, SqlValidatorScope parentScope) {
+ super(validator, id, enclosingNode, parentScope);
+ }
+ }
+
+ /**
+ * Namespace for an INSERT statement.
+ */
+ private static class InsertNamespace extends DmlNamespace {
+ private final SqlInsert node;
+
+ InsertNamespace(SqlValidatorImpl validator, SqlInsert node,
+ SqlNode enclosingNode, SqlValidatorScope parentScope) {
+ super(validator, node.getTargetTable(), enclosingNode, parentScope);
+ this.node = Objects.requireNonNull(node);
+ }
+
+ public SqlInsert getNode() {
+ return node;
+ }
+ }
+
+ /**
+ * Namespace for an UPDATE statement.
+ */
+ private static class UpdateNamespace extends DmlNamespace {
+ private final SqlUpdate node;
+
+ UpdateNamespace(SqlValidatorImpl validator, SqlUpdate node,
+ SqlNode enclosingNode, SqlValidatorScope parentScope) {
+ super(validator, node.getTargetTable(), enclosingNode, parentScope);
+ this.node = Objects.requireNonNull(node);
+ }
+
+ public SqlUpdate getNode() {
+ return node;
+ }
+ }
+
+ /**
+ * Namespace for a DELETE statement.
+ */
+ private static class DeleteNamespace extends DmlNamespace {
+ private final SqlDelete node;
+
+ DeleteNamespace(SqlValidatorImpl validator, SqlDelete node,
+ SqlNode enclosingNode, SqlValidatorScope parentScope) {
+ super(validator, node.getTargetTable(), enclosingNode, parentScope);
+ this.node = Objects.requireNonNull(node);
+ }
+
+ public SqlDelete getNode() {
+ return node;
+ }
+ }
+
+ /**
+ * Namespace for a MERGE statement.
+ */
+ private static class MergeNamespace extends DmlNamespace {
+ private final SqlMerge node;
+
+ MergeNamespace(SqlValidatorImpl validator, SqlMerge node,
+ SqlNode enclosingNode, SqlValidatorScope parentScope) {
+ super(validator, node.getTargetTable(), enclosingNode, parentScope);
+ this.node = Objects.requireNonNull(node);
+ }
+
+ public SqlMerge getNode() {
+ return node;
+ }
+ }
+
+ /**
+ * retrieve pattern variables defined
+ */
+ private class PatternVarVisitor implements SqlVisitor<Void> {
+ private MatchRecognizeScope scope;
+ PatternVarVisitor(MatchRecognizeScope scope) {
+ this.scope = scope;
+ }
+
+ @Override public Void visit(SqlLiteral literal) {
+ return null;
+ }
+
+ @Override public Void visit(SqlCall call) {
+ for (int i = 0; i < call.getOperandList().size(); i++) {
+ call.getOperandList().get(i).accept(this);
+ }
+ return null;
+ }
+
+ @Override public Void visit(SqlNodeList nodeList) {
+ throw Util.needToImplement(nodeList);
+ }
+
+ @Override public Void visit(SqlIdentifier id) {
+ Preconditions.checkArgument(id.isSimple());
+ scope.addPatternVar(id.getSimple());
+ return null;
+ }
+
+ @Override public Void visit(SqlDataTypeSpec type) {
+ throw Util.needToImplement(type);
+ }
+
+ @Override public Void visit(SqlDynamicParam param) {
+ throw Util.needToImplement(param);
+ }
+
+ @Override public Void visit(SqlIntervalQualifier intervalQualifier) {
+ throw Util.needToImplement(intervalQualifier);
+ }
+ }
+
+ /**
+ * Visitor which derives the type of a given {@link SqlNode}.
+ *
+ * <p>Each method must return the derived type. This visitor is basically a
+ * single-use dispatcher; the visit is never recursive.
+ */
+ private class DeriveTypeVisitor implements SqlVisitor<RelDataType> {
+ private final SqlValidatorScope scope;
+
+ DeriveTypeVisitor(SqlValidatorScope scope) {
+ this.scope = scope;
+ }
+
+ public RelDataType visit(SqlLiteral literal) {
+ return literal.createSqlType(typeFactory);
+ }
+
+ public RelDataType visit(SqlCall call) {
+ final SqlOperator operator = call.getOperator();
+ return operator.deriveType(SqlValidatorImpl.this, scope, call);
+ }
+
+ public RelDataType visit(SqlNodeList nodeList) {
+ // Operand is of a type that we can't derive a type for. If the
+ // operand is of a peculiar type, such as a SqlNodeList, then you
+ // should override the operator's validateCall() method so that it
+ // doesn't try to validate that operand as an expression.
+ throw Util.needToImplement(nodeList);
+ }
+
+ public RelDataType visit(SqlIdentifier id) {
+ // First check for builtin functions which don't have parentheses,
+ // like "LOCALTIME".
+ final SqlCall call = makeNullaryCall(id);
+ if (call != null) {
+ return call.getOperator().validateOperands(
+ SqlValidatorImpl.this,
+ scope,
+ call);
+ }
+
+ RelDataType type = null;
+ if (!(scope instanceof EmptyScope)) {
+ id = scope.fullyQualify(id).identifier;
+ }
+
+ // Resolve the longest prefix of id that we can
+ int i;
+ for (i = id.names.size() - 1; i > 0; i--) {
+ // REVIEW jvs 9-June-2005: The name resolution rules used
+ // here are supposed to match SQL:2003 Part 2 Section 6.6
+ // (identifier chain), but we don't currently have enough
+ // information to get everything right. In particular,
+ // routine parameters are currently looked up via resolve;
+ // we could do a better job if they were looked up via
+ // resolveColumn.
+
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ final SqlValidatorScope.ResolvedImpl resolved =
+ new SqlValidatorScope.ResolvedImpl();
+ scope.resolve(id.names.subList(0, i), nameMatcher, false, resolved);
+ if (resolved.count() == 1) {
+ // There's a namespace with the name we seek.
+ final SqlValidatorScope.Resolve resolve = resolved.only();
+ type = resolve.rowType();
+ for (SqlValidatorScope.Step p : Util.skip(resolve.path.steps())) {
+ type = type.getFieldList().get(p.i).getType();
+ }
+ break;
+ }
+ }
+
+ // Give precedence to namespace found, unless there
+ // are no more identifier components.
+ if (type == null || id.names.size() == 1) {
+ // See if there's a column with the name we seek in
+ // precisely one of the namespaces in this scope.
+ RelDataType colType = scope.resolveColumn(id.names.get(0), id);
+ if (colType != null) {
+ type = colType;
+ }
+ ++i;
+ }
+
+ if (type == null) {
+ final SqlIdentifier last = id.getComponent(i - 1, i);
+ throw newValidationError(last,
+ RESOURCE.unknownIdentifier(last.toString()));
+ }
+
+ // Resolve rest of identifier
+ for (; i < id.names.size(); i++) {
+ String name = id.names.get(i);
+ final RelDataTypeField field;
+ if (name.equals("")) {
+ // The wildcard "*" is represented as an empty name. It never
+ // resolves to a field.
+ name = "*";
+ field = null;
+ } else {
+ final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
+ field = nameMatcher.field(type, name);
+ }
+ if (field == null) {
+ throw newValidationError(id.getComponent(i),
+ RESOURCE.unknownField(name));
+ }
+ type = field.getType();
+ }
+ type =
+ SqlTypeUtil.addCharsetAndCollation(
+ type,
+ getTypeFactory());
+ return type;
+ }
+
+ public RelDataType visit(SqlDataTypeSpec dataType) {
+ // Q. How can a data type have a type?
+ // A. When it appears in an expression. (Say as the 2nd arg to the
+ // CAST operator.)
+ validateDataType(dataType);
+ return dataType.deriveType(SqlValidatorImpl.this);
+ }
+
+ public RelDataType visit(SqlDynamicParam param) {
+ return unknownType;
+ }
+
+ public RelDataType visit(SqlIntervalQualifier intervalQualifier) {
+ return typeFactory.createSqlIntervalType(intervalQualifier);
+ }
+ }
+
+ /**
+ * Converts an expression into canonical form by fully-qualifying any
+ * identifiers.
+ */
+ private static class Expander extends SqlScopedShuttle {
+ protected final SqlValidatorImpl validator;
+
+ Expander(SqlValidatorImpl validator, SqlValidatorScope scope) {
+ super(scope);
+ this.validator = validator;
+ }
+
+ @Override public SqlNode visit(SqlIdentifier id) {
+ // First check for builtin functions which don't have
+ // parentheses, like "LOCALTIME".
+ final SqlCall call = validator.makeNullaryCall(id);
+ if (call != null) {
+ return call.accept(this);
+ }
+ final SqlIdentifier fqId = getScope().fullyQualify(id).identifier;
+ SqlNode expandedExpr = expandDynamicStar(id, fqId);
+ validator.setOriginal(expandedExpr, id);
+ return expandedExpr;
+ }
+
+ @Override protected SqlNode visitScoped(SqlCall call) {
+ switch (call.getKind()) {
+ case SCALAR_QUERY:
+ case CURRENT_VALUE:
+ case NEXT_VALUE:
+ case WITH:
+ return call;
+ }
+ // Only visits arguments which are expressions. We don't want to
+ // qualify non-expressions such as 'x' in 'empno * 5 AS x'.
+ ArgHandler<SqlNode> argHandler =
+ new CallCopyingArgHandler(call, false);
+ call.getOperator().acceptCall(this, call, true, argHandler);
+ final SqlNode result = argHandler.result();
+ validator.setOriginal(result, call);
+ return result;
+ }
+
+ protected SqlNode expandDynamicStar(SqlIdentifier id, SqlIdentifier fqId) {
+ if (DynamicRecordType.isDynamicStarColName(Util.last(fqId.names))
+ && !DynamicRecordType.isDynamicStarColName(Util.last(id.names))) {
+ // Convert a column ref into ITEM(*, 'col_name')
+ // for a dynamic star field in dynTable's rowType.
+ SqlNode[] inputs = new SqlNode[2];
+ inputs[0] = fqId;
+ inputs[1] = SqlLiteral.createCharString(
+ Util.last(id.names),
+ id.getParserPosition());
+ return new SqlBasicCall(
+ SqlStdOperatorTable.ITEM,
+ inputs,
+ id.getParserPosition());
+ }
+ return fqId;
+ }
+ }
+
+ /**
+ * Shuttle which walks over an expression in the ORDER BY clause, replacing
+ * usages of aliases with the underlying expression.
+ */
+ class OrderExpressionExpander extends SqlScopedShuttle {
+ private final List<String> aliasList;
+ private final SqlSelect select;
+ private final SqlNode root;
+
+ OrderExpressionExpander(SqlSelect select, SqlNode root) {
+ super(getOrderScope(select));
+ this.select = select;
+ this.root = root;
+ this.aliasList = getNamespace(select).getRowType().getFieldNames();
+ }
+
+ public SqlNode go() {
+ return root.accept(this);
+ }
+
+ public SqlNode visit(SqlLiteral literal) {
+ // Ordinal markers, e.g. 'select a, b from t order by 2'.
+ // Only recognize them if they are the whole expression,
+ // and if the dialect permits.
+ if (literal == root && getConformance().isSortByOrdinal()) {
+ switch (literal.getTypeName()) {
+ case DECIMAL:
+ case DOUBLE:
+ final int intValue = literal.intValue(false);
+ if (intValue >= 0) {
+ if (intValue < 1 || intValue > aliasList.size()) {
+ 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;
+ }
+ }
+
+ 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(
+ select.getSelectList(),
+ 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);
+ }
+
+ public SqlNode visit(SqlIdentifier id) {
+ // Aliases, e.g. 'select a as x, b from t order by x'.
+ if (id.isSimple()
+ && getConformance().isSortByAlias()) {
+ String alias = id.getSimple();
+ final SqlValidatorNamespace selectNs = getNamespace(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;
+ }
+
+ protected 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);
+ }
+ }
+
+ /**
+ * 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 boolean havingExpr;
+
+ ExtendedExpander(SqlValidatorImpl validator, SqlValidatorScope scope,
+ SqlSelect select, SqlNode root, boolean havingExpr) {
+ super(validator, scope);
+ this.select = select;
+ this.root = root;
+ this.havingExpr = havingExpr;
+ }
+
+ @Override public SqlNode visit(SqlIdentifier id) {
+ if (id.isSimple()
+ && (havingExpr
+ ? validator.getConformance().isHavingAlias()
+ : validator.getConformance().isGroupByAlias())) {
+ String name = id.getSimple();
+ SqlNode expr = null;
+ final SqlNameMatcher nameMatcher =
+ validator.catalogReader.nameMatcher();
+ int n = 0;
+ for (SqlNode s : select.getSelectList()) {
+ final String alias = SqlValidatorUtil.getAlias(s, -1);
+ 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));
+ }
+ if (havingExpr && validator.isAggregate(root)) {
+ return super.visit(id);
+ }
+ expr = stripAs(expr);
+ if (expr instanceof SqlIdentifier) {
+ SqlIdentifier sid = (SqlIdentifier) expr;
+ final SqlIdentifier fqId = getScope().fullyQualify(sid).identifier;
+ expr = expandDynamicStar(sid, fqId);
+ }
+ return expr;
+ }
+ return super.visit(id);
+ }
+
+ public SqlNode visit(SqlLiteral literal) {
+ if (havingExpr || !validator.getConformance().isGroupByOrdinal()) {
+ return super.visit(literal);
+ }
+ boolean isOrdinalLiteral = literal == root;
+ switch (root.getKind()) {
+ case GROUPING_SETS:
+ case ROLLUP:
+ case CUBE:
+ if (root instanceof SqlBasicCall) {
+ List<SqlNode> operandList = ((SqlBasicCall) root).getOperandList();
+ for (SqlNode node : operandList) {
+ if (node.equals(literal)) {
+ isOrdinalLiteral = true;
+ break;
+ }
+ }
+ }
+ break;
+ }
+ if (isOrdinalLiteral) {
+ switch (literal.getTypeName()) {
+ case DECIMAL:
+ case DOUBLE:
+ final int intValue = literal.intValue(false);
+ if (intValue >= 0) {
+ if (intValue < 1 || intValue > select.getSelectList().size()) {
+ throw validator.newValidationError(literal,
+ RESOURCE.orderByOrdinalOutOfRange());
+ }
+
+ // SQL ordinals are 1-based, but Sort's are 0-based
+ int ordinal = intValue - 1;
+ return SqlUtil.stripAs(select.getSelectList().get(ordinal));
+ }
+ break;
+ }
+ }
+
+ return super.visit(literal);
+ }
+ }
+
+ /** Information about an identifier in a particular scope. */
+ protected static class IdInfo {
+ public final SqlValidatorScope scope;
+ public final SqlIdentifier id;
+
+ public IdInfo(SqlValidatorScope scope, SqlIdentifier id) {
+ this.scope = scope;
+ this.id = id;
+ }
+ }
+
+ /**
+ * Utility object used to maintain information about the parameters in a
+ * function call.
+ */
+ protected static class FunctionParamInfo {
+ /**
+ * Maps a cursor (based on its position relative to other cursor
+ * parameters within a function call) to the SELECT associated with the
+ * cursor.
+ */
+ public final Map<Integer, SqlSelect> cursorPosToSelectMap;
+
+ /**
+ * Maps a column list parameter to the parent cursor parameter it
+ * references. The parameters are id'd by their names.
+ */
+ public final Map<String, String> columnListParamToParentCursorMap;
+
+ public FunctionParamInfo() {
+ cursorPosToSelectMap = new HashMap<>();
+ columnListParamToParentCursorMap = new HashMap<>();
+ }
+ }
+
+ /**
+ * Modify the nodes in navigation function
+ * such as FIRST, LAST, PREV AND NEXT.
+ */
+ private static class NavigationModifier extends SqlShuttle {
+ public SqlNode go(SqlNode node) {
+ return node.accept(this);
+ }
+ }
+
+ /**
+ * Shuttle that expands navigation expressions in a MATCH_RECOGNIZE clause.
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ * <li>{@code PREV(A.price + A.amount)} →
+ * {@code PREV(A.price) + PREV(A.amount)}
+ *
+ * <li>{@code FIRST(A.price * 2)} → {@code FIRST(A.PRICE) * 2}
+ * </ul>
+ */
+ private static class NavigationExpander extends NavigationModifier {
+ final SqlOperator op;
+ final SqlNode offset;
+
+ NavigationExpander() {
+ this(null, null);
+ }
+
+ NavigationExpander(SqlOperator operator, SqlNode offset) {
+ this.offset = offset;
+ this.op = operator;
+ }
+
+ @Override public SqlNode visit(SqlCall call) {
+ SqlKind kind = call.getKind();
+ List<SqlNode> operands = call.getOperandList();
+ List<SqlNode> newOperands = new ArrayList<>();
+
+ // This code is a workaround for CALCITE-2707
+ if (call.getFunctionQuantifier() != null
+ && call.getFunctionQuantifier().getValue() == SqlSelectKeyword.DISTINCT) {
+ final SqlParserPos pos = call.getParserPosition();
+ throw SqlUtil.newContextException(pos, Static.RESOURCE.functionQuantifierNotAllowed(call.toString()));
+ }
+ // This code is a workaround for CALCITE-2707
+
+ if (isLogicalNavigation(kind) || isPhysicalNavigation(kind)) {
+ SqlNode inner = operands.get(0);
+ SqlNode offset = operands.get(1);
+
+ // merge two straight prev/next, update offset
+ if (isPhysicalNavigation(kind)) {
+ SqlKind innerKind = inner.getKind();
+ if (isPhysicalNavigation(innerKind)) {
+ List<SqlNode> innerOperands = ((SqlCall) inner).getOperandList();
+ SqlNode innerOffset = innerOperands.get(1);
+ SqlOperator newOperator = innerKind == kind
+ ? SqlStdOperatorTable.PLUS : SqlStdOperatorTable.MINUS;
+ offset = newOperator.createCall(SqlParserPos.ZERO,
+ offset, innerOffset);
+ inner = call.getOperator().createCall(SqlParserPos.ZERO,
+ innerOperands.get(0), offset);
+ }
+ }
+ SqlNode newInnerNode =
+ inner.accept(new NavigationExpander(call.getOperator(), offset));
+ if (op != null) {
+ newInnerNode = op.createCall(SqlParserPos.ZERO, newInnerNode,
+ this.offset);
+ }
+ return newInnerNode;
+ }
+
+ if (operands.size() > 0) {
+ for (SqlNode node : operands) {
+ if (node != null) {
+ SqlNode newNode = node.accept(new NavigationExpander());
+ if (op != null) {
+ newNode = op.createCall(SqlParserPos.ZERO, newNode, offset);
+ }
+ newOperands.add(newNode);
+ } else {
+ newOperands.add(null);
+ }
+ }
+ return call.getOperator().createCall(SqlParserPos.ZERO, newOperands);
+ } else {
+ if (op == null) {
+ return call;
+ } else {
+ return op.createCall(SqlParserPos.ZERO, call, offset);
+ }
+ }
+ }
+
+ @Override public SqlNode visit(SqlIdentifier id) {
+ if (op == null) {
+ return id;
+ } else {
+ return op.createCall(SqlParserPos.ZERO, id, offset);
+ }
+ }
+ }
+
+ /**
+ * Shuttle that replaces {@code A as A.price > PREV(B.price)} with
+ * {@code PREV(A.price, 0) > LAST(B.price, 0)}.
+ *
+ * <p>Replacing {@code A.price} with {@code PREV(A.price, 0)} makes the
+ * implementation of
+ * {@link RexVisitor#visitPatternFieldRef(RexPatternFieldRef)} more unified.
+ * Otherwise, it's difficult to implement this method. If it returns the
+ * specified field, then the navigation such as {@code PREV(A.price, 1)}
+ * becomes impossible; if not, then comparisons such as
+ * {@code A.price > PREV(A.price, 1)} become meaningless.
+ */
+ private static class NavigationReplacer extends NavigationModifier {
+ private final String alpha;
+
+ NavigationReplacer(String alpha) {
+ this.alpha = alpha;
+ }
+
+ @Override public SqlNode visit(SqlCall call) {
+ SqlKind kind = call.getKind();
+ if (isLogicalNavigation(kind)
+ || isAggregation(kind)
+ || isRunningOrFinal(kind)) {
+ return call;
+ }
+
+ switch (kind) {
+ case PREV:
+ final List<SqlNode> operands = call.getOperandList();
+ if (operands.get(0) instanceof SqlIdentifier) {
+ String name = ((SqlIdentifier) operands.get(0)).names.get(0);
+ return name.equals(alpha) ? call
+ : SqlStdOperatorTable.LAST.createCall(SqlParserPos.ZERO, operands);
+ }
+ }
+ return super.visit(call);
+ }
+
+ @Override public SqlNode visit(SqlIdentifier id) {
+ if (id.isSimple()) {
+ return id;
+ }
+ SqlOperator operator = id.names.get(0).equals(alpha)
+ ? SqlStdOperatorTable.PREV : SqlStdOperatorTable.LAST;
+
+ return operator.createCall(SqlParserPos.ZERO, id,
+ SqlLiteral.createExactNumeric("0", SqlParserPos.ZERO));
+ }
+ }
+
+ /**
+ * Within one navigation function, the pattern var should be same
+ */
+ private class PatternValidator extends SqlBasicVisitor<Set<String>> {
+ private final boolean isMeasure;
+ int firstLastCount;
+ int prevNextCount;
+ int aggregateCount;
+
+ PatternValidator(boolean isMeasure) {
+ this(isMeasure, 0, 0, 0);
+ }
+
+ PatternValidator(boolean isMeasure, int firstLastCount, int prevNextCount,
+ int aggregateCount) {
+ this.isMeasure = isMeasure;
+ this.firstLastCount = firstLastCount;
+ this.prevNextCount = prevNextCount;
+ this.aggregateCount = aggregateCount;
+ }
+
+ @Override public Set<String> visit(SqlCall call) {
+ boolean isSingle = false;
+ Set<String> vars = new HashSet<>();
+ SqlKind kind = call.getKind();
+ List<SqlNode> operands = call.getOperandList();
+
+ if (isSingleVarRequired(kind)) {
+ isSingle = true;
+ if (isPhysicalNavigation(kind)) {
+ if (isMeasure) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternPrevFunctionInMeasure(call.toString()));
+ }
+ if (firstLastCount != 0) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternPrevFunctionOrder(call.toString()));
+ }
+ prevNextCount++;
+ } else if (isLogicalNavigation(kind)) {
+ if (firstLastCount != 0) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternPrevFunctionOrder(call.toString()));
+ }
+ firstLastCount++;
+ } else if (isAggregation(kind)) {
+ // cannot apply aggregation in PREV/NEXT, FIRST/LAST
+ if (firstLastCount != 0 || prevNextCount != 0) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternAggregationInNavigation(call.toString()));
+ }
+ if (kind == SqlKind.COUNT && call.getOperandList().size() > 1) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternCountFunctionArg());
+ }
+ aggregateCount++;
+ }
+ }
+
+ if (isRunningOrFinal(kind) && !isMeasure) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternRunningFunctionInDefine(call.toString()));
+ }
+
+ for (SqlNode node : operands) {
+ if (node != null) {
+ vars.addAll(
+ node.accept(
+ new PatternValidator(isMeasure, firstLastCount, prevNextCount,
+ aggregateCount)));
+ }
+ }
+
+ if (isSingle) {
+ switch (kind) {
+ case COUNT:
+ if (vars.size() > 1) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternCountFunctionArg());
+ }
+ break;
+ default:
+ if (operands.size() == 0
+ || !(operands.get(0) instanceof SqlCall)
+ || ((SqlCall) operands.get(0)).getOperator() != SqlStdOperatorTable.CLASSIFIER) {
+ if (vars.isEmpty()) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternFunctionNullCheck(call.toString()));
+ }
+ if (vars.size() != 1) {
+ throw newValidationError(call,
+ Static.RESOURCE.patternFunctionVariableCheck(call.toString()));
+ }
+ }
+ break;
+ }
+ }
+ return vars;
+ }
+
+ @Override public Set<String> visit(SqlIdentifier identifier) {
+ boolean check = prevNextCount > 0 || firstLastCount > 0 || aggregateCount > 0;
+ Set<String> vars = new HashSet<>();
+ if (identifier.names.size() > 1 && check) {
+ vars.add(identifier.names.get(0));
+ }
+ return vars;
+ }
+
+ @Override public Set<String> visit(SqlLiteral literal) {
+ return ImmutableSet.of();
+ }
+
+ @Override public Set<String> visit(SqlIntervalQualifier qualifier) {
+ return ImmutableSet.of();
+ }
+
+ @Override public Set<String> visit(SqlDataTypeSpec type) {
+ return ImmutableSet.of();
+ }
+
+ @Override public Set<String> visit(SqlDynamicParam param) {
+ return ImmutableSet.of();
+ }
+ }
+
+ /** Permutation of fields in NATURAL JOIN or USING. */
+ private class Permute {
+ final List<ImmutableIntList> sources;
+ final RelDataType rowType;
+ final boolean trivial;
+
+ Permute(SqlNode from, int offset) {
+ switch (from.getKind()) {
+ case JOIN:
+ final SqlJoin join = (SqlJoin) from;
+ final Permute left = new Permute(join.getLeft(), offset);
+ final int fieldCount =
+ getValidatedNodeType(join.getLeft()).getFieldList().size();
+ final Permute right =
+ new Permute(join.getRight(), offset + fieldCount);
+ final List<String> names = usingNames(join);
+ final List<ImmutableIntList> sources = new ArrayList<>();
+ final Set<ImmutableIntList> sourceSet = new HashSet<>();
+ final RelDataTypeFactory.Builder b = typeFactory.builder();
+ if (names != null) {
+ for (String name : names) {
+ final RelDataTypeField f = left.field(name);
+ final ImmutableIntList source = left.sources.get(f.getIndex());
+ sourceSet.add(source);
+ final RelDataTypeField f2 = right.field(name);
+ final ImmutableIntList source2 = right.sources.get(f2.getIndex());
+ sourceSet.add(source2);
+ sources.add(source.appendAll(source2));
+ final boolean nullable =
+ (f.getType().isNullable()
+ || join.getJoinType().generatesNullsOnLeft())
+ && (f2.getType().isNullable()
+ || join.getJoinType().generatesNullsOnRight());
+ b.add(f).nullable(nullable);
+ }
+ }
+ for (RelDataTypeField f : left.rowType.getFieldList()) {
+ final ImmutableIntList source = left.sources.get(f.getIndex());
+ if (sourceSet.add(source)) {
+ sources.add(source);
+ b.add(f);
+ }
+ }
+ for (RelDataTypeField f : right.rowType.getFieldList()) {
+ final ImmutableIntList source = right.sources.get(f.getIndex());
+ if (sourceSet.add(source)) {
+ sources.add(source);
+ b.add(f);
+ }
+ }
+ rowType = b.build();
+ this.sources = ImmutableList.copyOf(sources);
+ this.trivial = left.trivial
+ && right.trivial
+ && (names == null || names.isEmpty());
+ break;
+
+ default:
+ rowType = getValidatedNodeType(from);
+ this.sources = Functions.generate(rowType.getFieldCount(),
+ i -> ImmutableIntList.of(offset + i));
+ this.trivial = true;
+ }
+ }
+
+ private RelDataTypeField field(String name) {
+ return catalogReader.nameMatcher().field(rowType, name);
+ }
+
+ /** Returns the set of field names in the join condition specified by USING
+ * or implicitly by NATURAL, de-duplicated and in order. */
+ private List<String> usingNames(SqlJoin join) {
+ switch (join.getConditionType()) {
+ case USING:
+ final ImmutableList.Builder<String> list = ImmutableList.builder();
+ final Set<String> names = catalogReader.nameMatcher().createSet();
+ for (SqlNode node : (SqlNodeList) join.getCondition()) {
+ final String name = ((SqlIdentifier) node).getSimple();
+ if (names.add(name)) {
+ list.add(name);
+ }
+ }
+ return list.build();
+ case NONE:
+ if (join.isNatural()) {
+ final RelDataType t0 = getValidatedNodeType(join.getLeft());
+ final RelDataType t1 = getValidatedNodeType(join.getRight());
+ return SqlValidatorUtil.deriveNaturalJoinColumnList(
+ catalogReader.nameMatcher(), t0, t1);
+ }
+ }
+ return null;
+ }
+
+ /** Moves fields according to the permutation. */
+ public void permute(List<SqlNode> selectItems,
+ List<Map.Entry<String, RelDataType>> fields) {
+ if (trivial) {
+ return;
+ }
+
+ final List<SqlNode> oldSelectItems = ImmutableList.copyOf(selectItems);
+ selectItems.clear();
+ final List<Map.Entry<String, RelDataType>> oldFields =
+ ImmutableList.copyOf(fields);
+ fields.clear();
+ for (ImmutableIntList source : sources) {
+ final int p0 = source.get(0);
+ Map.Entry<String, RelDataType> field = oldFields.get(p0);
+ final String name = field.getKey();
+ RelDataType type = field.getValue();
+ SqlNode selectItem = oldSelectItems.get(p0);
+ for (int p1 : Util.skip(source)) {
+ final Map.Entry<String, RelDataType> field1 = oldFields.get(p1);
+ final SqlNode selectItem1 = oldSelectItems.get(p1);
+ final RelDataType type1 = field1.getValue();
+ // output is nullable only if both inputs are
+ final boolean nullable = type.isNullable() && type1.isNullable();
+ final RelDataType type2 =
+ SqlTypeUtil.leastRestrictiveForComparison(typeFactory, type,
+ type1);
+ selectItem =
+ SqlStdOperatorTable.AS.createCall(SqlParserPos.ZERO,
+ SqlStdOperatorTable.COALESCE.createCall(SqlParserPos.ZERO,
+ maybeCast(selectItem, type, type2),
+ maybeCast(selectItem1, type1, type2)),
+ new SqlIdentifier(name, SqlParserPos.ZERO));
+ type = typeFactory.createTypeWithNullability(type2, nullable);
+ }
+ fields.add(Pair.of(name, type));
+ selectItems.add(selectItem);
+ }
+ }
+ }
+
+ //~ Enums ------------------------------------------------------------------
+
+ /**
+ * Validation status.
+ */
+ public enum Status {
+ /**
+ * Validation has not started for this scope.
+ */
+ UNVALIDATED,
+
+ /**
+ * Validation is in progress for this scope.
+ */
+ IN_PROGRESS,
+
+ /**
+ * Validation has completed (perhaps unsuccessfully).
+ */
+ VALID
+ }
+
+}
+
+// End SqlValidatorImpl.java
diff --git a/flink-table/flink-table-planner-blink/src/test/scala/org/apache/flink/table/api/stream/sql/validation/MatchRecognizeValidationTest.scala b/flink-table/flink-table-planner-blink/src/test/scala/org/apache/flink/table/api/stream/sql/validation/MatchRecognizeValidationTest.scala
index 45d1406..7c06ae4 100644
--- a/flink-table/flink-table-planner-blink/src/test/scala/org/apache/flink/table/api/stream/sql/validation/MatchRecognizeValidationTest.scala
+++ b/flink-table/flink-table-planner-blink/src/test/scala/org/apache/flink/table/api/stream/sql/validation/MatchRecognizeValidationTest.scala
@@ -28,7 +28,7 @@ import org.apache.flink.table.planner.runtime.utils.JavaUserDefinedScalarFunctio
import org.apache.flink.table.planner.utils.TableTestBase
import org.apache.flink.types.Row
-import org.junit.{Ignore, Test}
+import org.junit.Test
import java.sql.Timestamp
@@ -313,8 +313,6 @@ class MatchRecognizeValidationTest extends TableTestBase {
streamUtil.tableEnv.sqlQuery(sqlQuery).toAppendStream[Row]
}
- @Ignore("Calcite doesn't throw exception when parse distinct aggregate, " +
- "and doesn't provide information about distinct")
@Test
def testDistinctAggregationsNotSupported(): Unit = {
thrown.expect(classOf[ValidationException])
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
new file mode 100644
index 0000000..414c928
--- /dev/null
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/validate/ParameterScope.java
@@ -0,0 +1,72 @@
+/*
+ * 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.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlNode;
+
+import java.util.Map;
+
+// This class is copied from Calcite's org.apache.calcite.sql.validate.ParameterScope,
+// can be removed after https://issues.apache.org/jira/browse/CALCITE-3476 is fixed.
+//
+// Modification:
+// - L66~L69: override resolveColumn method
+
+/**
+ * A scope which contains nothing besides a few parameters. Like
+ * {@link EmptyScope} (which is its base class), it has no parent scope.
+ *
+ * @see ParameterNamespace
+ */
+public class ParameterScope extends EmptyScope {
+ //~ Instance fields --------------------------------------------------------
+
+ /**
+ * Map from the simple names of the parameters to types of the parameters
+ * ({@link RelDataType}).
+ */
+ private final Map<String, RelDataType> nameToTypeMap;
+
+ //~ Constructors -----------------------------------------------------------
+
+ public ParameterScope(
+ SqlValidatorImpl validator,
+ Map<String, RelDataType> nameToTypeMap) {
+ super(validator);
+ this.nameToTypeMap = nameToTypeMap;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ public SqlQualified fullyQualify(SqlIdentifier identifier) {
+ return SqlQualified.create(this, 1, null, identifier);
+ }
+
+ public SqlValidatorScope getOperandScope(SqlCall call) {
+ return this;
+ }
+
+ @Override
+ public RelDataType resolveColumn(String name, SqlNode ctx) {
+ return nameToTypeMap.get(name);
+ }
+}
+
+// End ParameterScope.java
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
deleted file mode 100644
index 8d5388d..0000000
--- a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ /dev/null
@@ -1,2857 +0,0 @@
-/*
- * 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.sql2rel;
-
-import org.apache.flink.table.plan.rules.logical.FlinkFilterJoinRule;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Sets;
-import com.google.common.collect.SortedSetMultimap;
-import org.apache.calcite.linq4j.Ord;
-import org.apache.calcite.linq4j.function.Function2;
-import org.apache.calcite.plan.Context;
-import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptCostImpl;
-import org.apache.calcite.plan.RelOptRule;
-import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptUtil;
-import org.apache.calcite.plan.hep.HepPlanner;
-import org.apache.calcite.plan.hep.HepProgram;
-import org.apache.calcite.plan.hep.HepRelVertex;
-import org.apache.calcite.rel.BiRel;
-import org.apache.calcite.rel.RelCollation;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.RelShuttleImpl;
-import org.apache.calcite.rel.core.Aggregate;
-import org.apache.calcite.rel.core.AggregateCall;
-import org.apache.calcite.rel.core.Correlate;
-import org.apache.calcite.rel.core.CorrelationId;
-import org.apache.calcite.rel.core.Filter;
-import org.apache.calcite.rel.core.JoinRelType;
-import org.apache.calcite.rel.core.Project;
-import org.apache.calcite.rel.core.RelFactories;
-import org.apache.calcite.rel.core.Sort;
-import org.apache.calcite.rel.core.Values;
-import org.apache.calcite.rel.logical.LogicalAggregate;
-import org.apache.calcite.rel.logical.LogicalCorrelate;
-import org.apache.calcite.rel.logical.LogicalFilter;
-import org.apache.calcite.rel.logical.LogicalJoin;
-import org.apache.calcite.rel.logical.LogicalProject;
-import org.apache.calcite.rel.logical.LogicalSnapshot;
-import org.apache.calcite.rel.logical.LogicalSort;
-import org.apache.calcite.rel.metadata.RelMdUtil;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rel.rules.FilterCorrelateRule;
-import org.apache.calcite.rel.rules.FilterJoinRule;
-import org.apache.calcite.rel.rules.FilterProjectTransposeRule;
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelDataTypeField;
-import org.apache.calcite.rex.RexBuilder;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexCorrelVariable;
-import org.apache.calcite.rex.RexFieldAccess;
-import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexLiteral;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexShuttle;
-import org.apache.calcite.rex.RexSubQuery;
-import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.rex.RexVisitorImpl;
-import org.apache.calcite.sql.SqlExplainFormat;
-import org.apache.calcite.sql.SqlExplainLevel;
-import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.SqlOperator;
-import org.apache.calcite.sql.fun.SqlCountAggFunction;
-import org.apache.calcite.sql.fun.SqlSingleValueAggFunction;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.tools.RelBuilder;
-import org.apache.calcite.tools.RelBuilderFactory;
-import org.apache.calcite.util.Holder;
-import org.apache.calcite.util.ImmutableBitSet;
-import org.apache.calcite.util.Litmus;
-import org.apache.calcite.util.Pair;
-import org.apache.calcite.util.ReflectUtil;
-import org.apache.calcite.util.ReflectiveVisitor;
-import org.apache.calcite.util.Util;
-import org.apache.calcite.util.mapping.Mappings;
-import org.apache.calcite.util.trace.CalciteTrace;
-import org.slf4j.Logger;
-
-import javax.annotation.Nonnull;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Objects;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * This class is copied from Apache Calcite except that it supports SEMI/ANTI join.
- * NOTES: This file should be deleted when CALCITE-3169 and CALCITE-3170 are fixed,
- * and please make sure to synchronize with RelDecorrelator in blink planner when changing this class.
- * Modification:
- * 1. lines changed (249-251)
- * 2. lines changed (271-278)
- * 3. lines changed (1214-1215)
- */
-
-/**
- * RelDecorrelator replaces all correlated expressions (corExp) in a relational
- * expression (RelNode) tree with non-correlated expressions that are produced
- * from joining the RelNode that produces the corExp with the RelNode that
- * references it.
- *
- * <p>TODO:</p>
- * <ul>
- * <li>replace {@code CorelMap} constructor parameter with a RelNode
- * <li>make {@link #currentRel} immutable (would require a fresh
- * RelDecorrelator for each node being decorrelated)</li>
- * <li>make fields of {@code CorelMap} immutable</li>
- * <li>make sub-class rules static, and have them create their own
- * de-correlator</li>
- * </ul>
- */
-public class RelDecorrelator implements ReflectiveVisitor {
- //~ Static fields/initializers ---------------------------------------------
-
- private static final Logger SQL2REL_LOGGER =
- CalciteTrace.getSqlToRelTracer();
-
- //~ Instance fields --------------------------------------------------------
-
- private final RelBuilder relBuilder;
-
- // map built during translation
- private CorelMap cm;
-
- private final ReflectUtil.MethodDispatcher<Frame> dispatcher =
- ReflectUtil.createMethodDispatcher(Frame.class, this, "decorrelateRel",
- RelNode.class);
-
- // The rel which is being visited
- private RelNode currentRel;
-
- private final Context context;
-
- /** Built during decorrelation, of rel to all the newly created correlated
- * variables in its output, and to map old input positions to new input
- * positions. This is from the view point of the parent rel of a new rel. */
- private final Map<RelNode, Frame> map = new HashMap<>();
-
- private final HashSet<LogicalCorrelate> generatedCorRels = new HashSet<>();
-
- //~ Constructors -----------------------------------------------------------
-
- private RelDecorrelator(
- CorelMap cm,
- Context context,
- RelBuilder relBuilder) {
- this.cm = cm;
- this.context = context;
- this.relBuilder = relBuilder;
- }
-
- //~ Methods ----------------------------------------------------------------
-
- @Deprecated // to be removed before 2.0
- public static RelNode decorrelateQuery(RelNode rootRel) {
- final RelBuilder relBuilder =
- RelFactories.LOGICAL_BUILDER.create(rootRel.getCluster(), null);
- return decorrelateQuery(rootRel, relBuilder);
- }
-
- /** Decorrelates a query.
- *
- * <p>This is the main entry point to {@code RelDecorrelator}.
- *
- * @param rootRel Root node of the query
- * @param relBuilder Builder for relational expressions
- *
- * @return Equivalent query with all
- * {@link LogicalCorrelate} instances removed
- */
- public static RelNode decorrelateQuery(RelNode rootRel,
- RelBuilder relBuilder) {
- final CorelMap corelMap = new CorelMapBuilder().build(rootRel);
- if (!corelMap.hasCorrelation()) {
- return rootRel;
- }
-
- final RelOptCluster cluster = rootRel.getCluster();
- final RelDecorrelator decorrelator =
- new RelDecorrelator(corelMap,
- cluster.getPlanner().getContext(), relBuilder);
-
- RelNode newRootRel = decorrelator.removeCorrelationViaRule(rootRel);
-
- if (SQL2REL_LOGGER.isDebugEnabled()) {
- SQL2REL_LOGGER.debug(
- RelOptUtil.dumpPlan("Plan after removing Correlator", newRootRel,
- SqlExplainFormat.TEXT, SqlExplainLevel.EXPPLAN_ATTRIBUTES));
- }
-
- if (!decorrelator.cm.mapCorToCorRel.isEmpty()) {
- newRootRel = decorrelator.decorrelate(newRootRel);
- }
-
- return newRootRel;
- }
-
- private void setCurrent(RelNode root, LogicalCorrelate corRel) {
- currentRel = corRel;
- if (corRel != null) {
- cm = new CorelMapBuilder().build(Util.first(root, corRel));
- }
- }
-
- private RelBuilderFactory relBuilderFactory() {
- return RelBuilder.proto(relBuilder);
- }
-
- private RelNode decorrelate(RelNode root) {
- // first adjust count() expression if any
- final RelBuilderFactory f = relBuilderFactory();
- HepProgram program = HepProgram.builder()
- .addRuleInstance(new AdjustProjectForCountAggregateRule(false, f))
- .addRuleInstance(new AdjustProjectForCountAggregateRule(true, f))
- .addRuleInstance(
- // use FilterJoinRule instead of FlinkFilterJoinRule while CALCITE-3170 is fixed
- new FlinkFilterJoinRule.FlinkFilterIntoJoinRule(true, f,
- FlinkFilterJoinRule.TRUE_PREDICATE))
- .addRuleInstance(
- new FilterProjectTransposeRule(Filter.class, Project.class, true,
- true, f))
- .addRuleInstance(new FilterCorrelateRule(f))
- .build();
-
- HepPlanner planner = createPlanner(program);
-
- planner.setRoot(root);
- root = planner.findBestExp();
-
- // Perform decorrelation.
- map.clear();
-
- final Frame frame = getInvoke(root, null);
- if (frame != null) {
- // has been rewritten; apply rules post-decorrelation
- final HepProgram program2 = HepProgram.builder()
- .addRuleInstance(
- // use FilterJoinRule instead of FlinkFilterJoinRule while CALCITE-3170 is fixed
- new FlinkFilterJoinRule.FlinkFilterIntoJoinRule(
- true, f,
- FlinkFilterJoinRule.TRUE_PREDICATE))
- .addRuleInstance(
- new FlinkFilterJoinRule.FlinkJoinConditionPushRule(
- f,
- FlinkFilterJoinRule.TRUE_PREDICATE))
- .build();
-
- final HepPlanner planner2 = createPlanner(program2);
- final RelNode newRoot = frame.r;
- planner2.setRoot(newRoot);
- return planner2.findBestExp();
- }
-
- return root;
- }
-
- private Function2<RelNode, RelNode, Void> createCopyHook() {
- return (oldNode, newNode) -> {
- if (cm.mapRefRelToCorRef.containsKey(oldNode)) {
- cm.mapRefRelToCorRef.putAll(newNode,
- cm.mapRefRelToCorRef.get(oldNode));
- }
- if (oldNode instanceof LogicalCorrelate
- && newNode instanceof LogicalCorrelate) {
- LogicalCorrelate oldCor = (LogicalCorrelate) oldNode;
- CorrelationId c = oldCor.getCorrelationId();
- if (cm.mapCorToCorRel.get(c) == oldNode) {
- cm.mapCorToCorRel.put(c, newNode);
- }
-
- if (generatedCorRels.contains(oldNode)) {
- generatedCorRels.add((LogicalCorrelate) newNode);
- }
- }
- return null;
- };
- }
-
- private HepPlanner createPlanner(HepProgram program) {
- // Create a planner with a hook to update the mapping tables when a
- // node is copied when it is registered.
- return new HepPlanner(
- program,
- context,
- true,
- createCopyHook(),
- RelOptCostImpl.FACTORY);
- }
-
- public RelNode removeCorrelationViaRule(RelNode root) {
- final RelBuilderFactory f = relBuilderFactory();
- HepProgram program = HepProgram.builder()
- .addRuleInstance(new RemoveSingleAggregateRule(f))
- .addRuleInstance(new RemoveCorrelationForScalarProjectRule(f))
- .addRuleInstance(new RemoveCorrelationForScalarAggregateRule(f))
- .build();
-
- HepPlanner planner = createPlanner(program);
-
- planner.setRoot(root);
- return planner.findBestExp();
- }
-
- protected RexNode decorrelateExpr(RelNode currentRel,
- Map<RelNode, Frame> map, CorelMap cm, RexNode exp) {
- DecorrelateRexShuttle shuttle =
- new DecorrelateRexShuttle(currentRel, map, cm);
- return exp.accept(shuttle);
- }
-
- protected RexNode removeCorrelationExpr(
- RexNode exp,
- boolean projectPulledAboveLeftCorrelator) {
- RemoveCorrelationRexShuttle shuttle =
- new RemoveCorrelationRexShuttle(relBuilder.getRexBuilder(),
- projectPulledAboveLeftCorrelator, null, ImmutableSet.of());
- return exp.accept(shuttle);
- }
-
- protected RexNode removeCorrelationExpr(
- RexNode exp,
- boolean projectPulledAboveLeftCorrelator,
- RexInputRef nullIndicator) {
- RemoveCorrelationRexShuttle shuttle =
- new RemoveCorrelationRexShuttle(relBuilder.getRexBuilder(),
- projectPulledAboveLeftCorrelator, nullIndicator,
- ImmutableSet.of());
- return exp.accept(shuttle);
- }
-
- protected RexNode removeCorrelationExpr(
- RexNode exp,
- boolean projectPulledAboveLeftCorrelator,
- Set<Integer> isCount) {
- RemoveCorrelationRexShuttle shuttle =
- new RemoveCorrelationRexShuttle(relBuilder.getRexBuilder(),
- projectPulledAboveLeftCorrelator, null, isCount);
- return exp.accept(shuttle);
- }
-
- /** Fallback if none of the other {@code decorrelateRel} methods match. */
- public Frame decorrelateRel(RelNode rel) {
- RelNode newRel = rel.copy(rel.getTraitSet(), rel.getInputs());
-
- if (rel.getInputs().size() > 0) {
- List<RelNode> oldInputs = rel.getInputs();
- List<RelNode> newInputs = new ArrayList<>();
- for (int i = 0; i < oldInputs.size(); ++i) {
- final Frame frame = getInvoke(oldInputs.get(i), rel);
- if (frame == null || !frame.corDefOutputs.isEmpty()) {
- // if input is not rewritten, or if it produces correlated
- // variables, terminate rewrite
- return null;
- }
- newInputs.add(frame.r);
- newRel.replaceInput(i, frame.r);
- }
-
- if (!Util.equalShallow(oldInputs, newInputs)) {
- newRel = rel.copy(rel.getTraitSet(), newInputs);
- }
- }
-
- // the output position should not change since there are no corVars
- // coming from below.
- return register(rel, newRel, identityMap(rel.getRowType().getFieldCount()),
- ImmutableSortedMap.of());
- }
-
- /**
- * Rewrite Sort.
- *
- * @param rel Sort to be rewritten
- */
- public Frame decorrelateRel(Sort rel) {
- //
- // Rewrite logic:
- //
- // 1. change the collations field to reference the new input.
- //
-
- // Sort itself should not reference corVars.
- assert !cm.mapRefRelToCorRef.containsKey(rel);
-
- // Sort only references field positions in collations field.
- // The collations field in the newRel now need to refer to the
- // new output positions in its input.
- // Its output does not change the input ordering, so there's no
- // need to call propagateExpr.
-
- final RelNode oldInput = rel.getInput();
- final Frame frame = getInvoke(oldInput, rel);
- if (frame == null) {
- // If input has not been rewritten, do not rewrite this rel.
- return null;
- }
- final RelNode newInput = frame.r;
-
- Mappings.TargetMapping mapping =
- Mappings.target(frame.oldToNewOutputs,
- oldInput.getRowType().getFieldCount(),
- newInput.getRowType().getFieldCount());
-
- RelCollation oldCollation = rel.getCollation();
- RelCollation newCollation = RexUtil.apply(mapping, oldCollation);
-
- final Sort newSort =
- LogicalSort.create(newInput, newCollation, rel.offset, rel.fetch);
-
- // Sort does not change input ordering
- return register(rel, newSort, frame.oldToNewOutputs, frame.corDefOutputs);
- }
-
- /**
- * Rewrites a {@link Values}.
- *
- * @param rel Values to be rewritten
- */
- public Frame decorrelateRel(Values rel) {
- // There are no inputs, so rel does not need to be changed.
- return null;
- }
-
- /**
- * Rewrites a {@link LogicalAggregate}.
- *
- * @param rel Aggregate to rewrite
- */
- public Frame decorrelateRel(LogicalAggregate rel) {
- //
- // Rewrite logic:
- //
- // 1. Permute the group by keys to the front.
- // 2. If the input of an aggregate produces correlated variables,
- // add them to the group list.
- // 3. Change aggCalls to reference the new project.
- //
-
- // Aggregate itself should not reference corVars.
- assert !cm.mapRefRelToCorRef.containsKey(rel);
-
- final RelNode oldInput = rel.getInput();
- final Frame frame = getInvoke(oldInput, rel);
- if (frame == null) {
- // If input has not been rewritten, do not rewrite this rel.
- return null;
- }
- final RelNode newInput = frame.r;
-
- // aggregate outputs mapping: group keys and aggregates
- final Map<Integer, Integer> outputMap = new HashMap<>();
-
- // map from newInput
- final Map<Integer, Integer> mapNewInputToProjOutputs = new HashMap<>();
- final int oldGroupKeyCount = rel.getGroupSet().cardinality();
-
- // Project projects the original expressions,
- // plus any correlated variables the input wants to pass along.
- final List<Pair<RexNode, String>> projects = new ArrayList<>();
-
- List<RelDataTypeField> newInputOutput =
- newInput.getRowType().getFieldList();
-
- int newPos = 0;
-
- // oldInput has the original group by keys in the front.
- final NavigableMap<Integer, RexLiteral> omittedConstants = new TreeMap<>();
- for (int i = 0; i < oldGroupKeyCount; i++) {
- final RexLiteral constant = projectedLiteral(newInput, i);
- if (constant != null) {
- // Exclude constants. Aggregate({true}) occurs because Aggregate({})
- // would generate 1 row even when applied to an empty table.
- omittedConstants.put(i, constant);
- continue;
- }
-
- // add mapping of group keys.
- outputMap.put(i, newPos);
- int newInputPos = frame.oldToNewOutputs.get(i);
- projects.add(RexInputRef.of2(newInputPos, newInputOutput));
- mapNewInputToProjOutputs.put(newInputPos, newPos);
- newPos++;
- }
-
- final SortedMap<CorDef, Integer> corDefOutputs = new TreeMap<>();
- if (!frame.corDefOutputs.isEmpty()) {
- // If input produces correlated variables, move them to the front,
- // right after any existing GROUP BY fields.
-
- // Now add the corVars from the input, starting from
- // position oldGroupKeyCount.
- for (Map.Entry<CorDef, Integer> entry : frame.corDefOutputs.entrySet()) {
- projects.add(RexInputRef.of2(entry.getValue(), newInputOutput));
-
- corDefOutputs.put(entry.getKey(), newPos);
- mapNewInputToProjOutputs.put(entry.getValue(), newPos);
- newPos++;
- }
- }
-
- // add the remaining fields
- final int newGroupKeyCount = newPos;
- for (int i = 0; i < newInputOutput.size(); i++) {
- if (!mapNewInputToProjOutputs.containsKey(i)) {
- projects.add(RexInputRef.of2(i, newInputOutput));
- mapNewInputToProjOutputs.put(i, newPos);
- newPos++;
- }
- }
-
- assert newPos == newInputOutput.size();
-
- // This Project will be what the old input maps to,
- // replacing any previous mapping from old input).
- RelNode newProject = relBuilder.push(newInput)
- .projectNamed(Pair.left(projects), Pair.right(projects), true)
- .build();
-
- // update mappings:
- // oldInput ----> newInput
- //
- // newProject
- // |
- // oldInput ----> newInput
- //
- // is transformed to
- //
- // oldInput ----> newProject
- // |
- // newInput
- Map<Integer, Integer> combinedMap = new HashMap<>();
-
- for (Integer oldInputPos : frame.oldToNewOutputs.keySet()) {
- combinedMap.put(oldInputPos,
- mapNewInputToProjOutputs.get(
- frame.oldToNewOutputs.get(oldInputPos)));
- }
-
- register(oldInput, newProject, combinedMap, corDefOutputs);
-
- // now it's time to rewrite the Aggregate
- final ImmutableBitSet newGroupSet = ImmutableBitSet.range(newGroupKeyCount);
- List<AggregateCall> newAggCalls = new ArrayList<>();
- List<AggregateCall> oldAggCalls = rel.getAggCallList();
-
- ImmutableList<ImmutableBitSet> newGroupSets = null;
- if (rel.getGroupType() != Aggregate.Group.SIMPLE) {
- final ImmutableBitSet addedGroupSet =
- ImmutableBitSet.range(oldGroupKeyCount, newGroupKeyCount);
- final Iterable<ImmutableBitSet> tmpGroupSets =
- Iterables.transform(rel.getGroupSets(),
- bitSet -> bitSet.union(addedGroupSet));
- newGroupSets = ImmutableBitSet.ORDERING.immutableSortedCopy(tmpGroupSets);
- }
-
- int oldInputOutputFieldCount = rel.getGroupSet().cardinality();
- int newInputOutputFieldCount = newGroupSet.cardinality();
-
- int i = -1;
- for (AggregateCall oldAggCall : oldAggCalls) {
- ++i;
- List<Integer> oldAggArgs = oldAggCall.getArgList();
-
- List<Integer> aggArgs = new ArrayList<>();
-
- // Adjust the Aggregate argument positions.
- // Note Aggregate does not change input ordering, so the input
- // output position mapping can be used to derive the new positions
- // for the argument.
- for (int oldPos : oldAggArgs) {
- aggArgs.add(combinedMap.get(oldPos));
- }
- final int filterArg = oldAggCall.filterArg < 0 ? oldAggCall.filterArg
- : combinedMap.get(oldAggCall.filterArg);
-
- newAggCalls.add(
- oldAggCall.adaptTo(newProject, aggArgs, filterArg,
- oldGroupKeyCount, newGroupKeyCount));
-
- // The old to new output position mapping will be the same as that
- // of newProject, plus any aggregates that the oldAgg produces.
- outputMap.put(
- oldInputOutputFieldCount + i,
- newInputOutputFieldCount + i);
- }
-
- relBuilder.push(
- LogicalAggregate.create(newProject, newGroupSet, newGroupSets, newAggCalls));
-
- if (!omittedConstants.isEmpty()) {
- final List<RexNode> postProjects = new ArrayList<>(relBuilder.fields());
- for (Map.Entry<Integer, RexLiteral> entry
- : omittedConstants.descendingMap().entrySet()) {
- int index = entry.getKey() + frame.corDefOutputs.size();
- postProjects.add(index, entry.getValue());
- // Shift the outputs whose index equals with or bigger than the added index
- // with 1 offset.
- shiftMapping(outputMap, index, 1);
- // Then add the constant key mapping.
- outputMap.put(entry.getKey(), index);
- }
- relBuilder.project(postProjects);
- }
-
- // Aggregate does not change input ordering so corVars will be
- // located at the same position as the input newProject.
- return register(rel, relBuilder.build(), outputMap, corDefOutputs);
- }
-
- /**
- * Shift the mapping to fixed offset from the {@code startIndex}.
- * @param mapping the original mapping
- * @param startIndex any output whose index equals with or bigger than the starting index
- * would be shift
- * @param offset shift offset
- */
- private static void shiftMapping(Map<Integer, Integer> mapping, int startIndex, int offset) {
- for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
- if (entry.getValue() >= startIndex) {
- mapping.put(entry.getKey(), entry.getValue() + offset);
- } else {
- mapping.put(entry.getKey(), entry.getValue());
- }
- }
- }
-
- public Frame getInvoke(RelNode r, RelNode parent) {
- final Frame frame = dispatcher.invoke(r);
- if (frame != null) {
- map.put(r, frame);
- }
- currentRel = parent;
- return frame;
- }
-
- /** Returns a literal output field, or null if it is not literal. */
- private static RexLiteral projectedLiteral(RelNode rel, int i) {
- if (rel instanceof Project) {
- final Project project = (Project) rel;
- final RexNode node = project.getProjects().get(i);
- if (node instanceof RexLiteral) {
- return (RexLiteral) node;
- }
- }
- return null;
- }
-
- /**
- * Rewrite LogicalProject.
- *
- * @param rel the project rel to rewrite
- */
- public Frame decorrelateRel(LogicalProject rel) {
- //
- // Rewrite logic:
- //
- // 1. Pass along any correlated variables coming from the input.
- //
-
- final RelNode oldInput = rel.getInput();
- Frame frame = getInvoke(oldInput, rel);
- if (frame == null) {
- // If input has not been rewritten, do not rewrite this rel.
- return null;
- }
- final List<RexNode> oldProjects = rel.getProjects();
- final List<RelDataTypeField> relOutput = rel.getRowType().getFieldList();
-
- // Project projects the original expressions,
- // plus any correlated variables the input wants to pass along.
- final List<Pair<RexNode, String>> projects = new ArrayList<>();
-
- // If this Project has correlated reference, create value generator
- // and produce the correlated variables in the new output.
- if (cm.mapRefRelToCorRef.containsKey(rel)) {
- frame = decorrelateInputWithValueGenerator(rel, frame);
- }
-
- // Project projects the original expressions
- final Map<Integer, Integer> mapOldToNewOutputs = new HashMap<>();
- int newPos;
- for (newPos = 0; newPos < oldProjects.size(); newPos++) {
- projects.add(
- newPos,
- Pair.of(
- decorrelateExpr(currentRel, map, cm, oldProjects.get(newPos)),
- relOutput.get(newPos).getName()));
- mapOldToNewOutputs.put(newPos, newPos);
- }
-
- // Project any correlated variables the input wants to pass along.
- final SortedMap<CorDef, Integer> corDefOutputs = new TreeMap<>();
- for (Map.Entry<CorDef, Integer> entry : frame.corDefOutputs.entrySet()) {
- projects.add(
- RexInputRef.of2(entry.getValue(),
- frame.r.getRowType().getFieldList()));
- corDefOutputs.put(entry.getKey(), newPos);
- newPos++;
- }
-
- RelNode newProject = relBuilder.push(frame.r)
- .projectNamed(Pair.left(projects), Pair.right(projects), true)
- .build();
-
- return register(rel, newProject, mapOldToNewOutputs, corDefOutputs);
- }
-
- /**
- * Create RelNode tree that produces a list of correlated variables.
- *
- * @param correlations correlated variables to generate
- * @param valueGenFieldOffset offset in the output that generated columns
- * will start
- * @param corDefOutputs output positions for the correlated variables
- * generated
- * @return RelNode the root of the resultant RelNode tree
- */
- private RelNode createValueGenerator(
- Iterable<CorRef> correlations,
- int valueGenFieldOffset,
- SortedMap<CorDef, Integer> corDefOutputs) {
- final Map<RelNode, List<Integer>> mapNewInputToOutputs = new HashMap<>();
-
- final Map<RelNode, Integer> mapNewInputToNewOffset = new HashMap<>();
-
- // Input provides the definition of a correlated variable.
- // Add to map all the referenced positions (relative to each input rel).
- for (CorRef corVar : correlations) {
- final int oldCorVarOffset = corVar.field;
-
- final RelNode oldInput = getCorRel(corVar);
- assert oldInput != null;
- final Frame frame = getFrame(oldInput, true);
- assert frame != null;
- final RelNode newInput = frame.r;
-
- final List<Integer> newLocalOutputs;
- if (!mapNewInputToOutputs.containsKey(newInput)) {
- newLocalOutputs = new ArrayList<>();
- } else {
- newLocalOutputs = mapNewInputToOutputs.get(newInput);
- }
-
- final int newCorVarOffset = frame.oldToNewOutputs.get(oldCorVarOffset);
-
- // Add all unique positions referenced.
- if (!newLocalOutputs.contains(newCorVarOffset)) {
- newLocalOutputs.add(newCorVarOffset);
- }
- mapNewInputToOutputs.put(newInput, newLocalOutputs);
- }
-
- int offset = 0;
-
- // Project only the correlated fields out of each input
- // and join the project together.
- // To make sure the plan does not change in terms of join order,
- // join these rels based on their occurrence in corVar list which
- // is sorted.
- final Set<RelNode> joinedInputs = new HashSet<>();
-
- RelNode r = null;
- for (CorRef corVar : correlations) {
- final RelNode oldInput = getCorRel(corVar);
- assert oldInput != null;
- final RelNode newInput = getFrame(oldInput, true).r;
- assert newInput != null;
-
- if (!joinedInputs.contains(newInput)) {
- RelNode project =
- RelOptUtil.createProject(newInput,
- mapNewInputToOutputs.get(newInput));
- RelNode distinct = relBuilder.push(project)
- .distinct()
- .build();
- RelOptCluster cluster = distinct.getCluster();
-
- joinedInputs.add(newInput);
- mapNewInputToNewOffset.put(newInput, offset);
- offset += distinct.getRowType().getFieldCount();
-
- if (r == null) {
- r = distinct;
- } else {
- r =
- LogicalJoin.create(r, distinct,
- cluster.getRexBuilder().makeLiteral(true),
- ImmutableSet.of(), JoinRelType.INNER);
- }
- }
- }
-
- // Translate the positions of correlated variables to be relative to
- // the join output, leaving room for valueGenFieldOffset because
- // valueGenerators are joined with the original left input of the rel
- // referencing correlated variables.
- for (CorRef corRef : correlations) {
- // The first input of a Correlate is always the rel defining
- // the correlated variables.
- final RelNode oldInput = getCorRel(corRef);
- assert oldInput != null;
- final Frame frame = getFrame(oldInput, true);
- final RelNode newInput = frame.r;
- assert newInput != null;
-
- final List<Integer> newLocalOutputs = mapNewInputToOutputs.get(newInput);
-
- final int newLocalOutput = frame.oldToNewOutputs.get(corRef.field);
-
- // newOutput is the index of the corVar in the referenced
- // position list plus the offset of referenced position list of
- // each newInput.
- final int newOutput =
- newLocalOutputs.indexOf(newLocalOutput)
- + mapNewInputToNewOffset.get(newInput)
- + valueGenFieldOffset;
-
- corDefOutputs.put(corRef.def(), newOutput);
- }
-
- return r;
- }
-
- private Frame getFrame(RelNode r, boolean safe) {
- final Frame frame = map.get(r);
- if (frame == null && safe) {
- return new Frame(r, r, ImmutableSortedMap.of(),
- identityMap(r.getRowType().getFieldCount()));
- }
- return frame;
- }
-
- private RelNode getCorRel(CorRef corVar) {
- final RelNode r = cm.mapCorToCorRel.get(corVar.corr);
- return r.getInput(0);
- }
-
- /** Adds a value generator to satisfy the correlating variables used by
- * a relational expression, if those variables are not already provided by
- * its input. */
- private Frame maybeAddValueGenerator(RelNode rel, Frame frame) {
- final CorelMap cm1 = new CorelMapBuilder().build(frame.r, rel);
- if (!cm1.mapRefRelToCorRef.containsKey(rel)) {
- return frame;
- }
- final Collection<CorRef> needs = cm1.mapRefRelToCorRef.get(rel);
- final ImmutableSortedSet<CorDef> haves = frame.corDefOutputs.keySet();
- if (hasAll(needs, haves)) {
- return frame;
- }
- return decorrelateInputWithValueGenerator(rel, frame);
- }
-
- /** Returns whether all of a collection of {@link CorRef}s are satisfied
- * by at least one of a collection of {@link CorDef}s. */
- private boolean hasAll(Collection<CorRef> corRefs,
- Collection<CorDef> corDefs) {
- for (CorRef corRef : corRefs) {
- if (!has(corDefs, corRef)) {
- return false;
- }
- }
- return true;
- }
-
- /** Returns whether a {@link CorrelationId} is satisfied by at least one of a
- * collection of {@link CorDef}s. */
- private boolean has(Collection<CorDef> corDefs, CorRef corr) {
- for (CorDef corDef : corDefs) {
- if (corDef.corr.equals(corr.corr) && corDef.field == corr.field) {
- return true;
- }
- }
- return false;
- }
-
- private Frame decorrelateInputWithValueGenerator(RelNode rel, Frame frame) {
- // currently only handles one input
- assert rel.getInputs().size() == 1;
- RelNode oldInput = frame.r;
-
- final SortedMap<CorDef, Integer> corDefOutputs =
- new TreeMap<>(frame.corDefOutputs);
-
- final Collection<CorRef> corVarList = cm.mapRefRelToCorRef.get(rel);
-
- // Try to populate correlation variables using local fields.
- // This means that we do not need a value generator.
- if (rel instanceof Filter) {
- SortedMap<CorDef, Integer> map = new TreeMap<>();
- List<RexNode> projects = new ArrayList<>();
- for (CorRef correlation : corVarList) {
- final CorDef def = correlation.def();
- if (corDefOutputs.containsKey(def) || map.containsKey(def)) {
- continue;
- }
- try {
- findCorrelationEquivalent(correlation, ((Filter) rel).getCondition());
- } catch (Util.FoundOne e) {
- if (e.getNode() instanceof RexInputRef) {
- map.put(def, ((RexInputRef) e.getNode()).getIndex());
- } else {
- map.put(def,
- frame.r.getRowType().getFieldCount() + projects.size());
- projects.add((RexNode) e.getNode());
- }
- }
- }
- // If all correlation variables are now satisfied, skip creating a value
- // generator.
- if (map.size() == corVarList.size()) {
- map.putAll(frame.corDefOutputs);
- final RelNode r;
- if (!projects.isEmpty()) {
- relBuilder.push(oldInput)
- .project(Iterables.concat(relBuilder.fields(), projects));
- r = relBuilder.build();
- } else {
- r = oldInput;
- }
- return register(rel.getInput(0), r,
- frame.oldToNewOutputs, map);
- }
- }
-
- int leftInputOutputCount = frame.r.getRowType().getFieldCount();
-
- // can directly add positions into corDefOutputs since join
- // does not change the output ordering from the inputs.
- RelNode valueGen =
- createValueGenerator(corVarList, leftInputOutputCount, corDefOutputs);
-
- RelNode join =
- LogicalJoin.create(frame.r, valueGen, relBuilder.literal(true),
- ImmutableSet.of(), JoinRelType.INNER);
-
- // Join or Filter does not change the old input ordering. All
- // input fields from newLeftInput (i.e. the original input to the old
- // Filter) are in the output and in the same position.
- return register(rel.getInput(0), join, frame.oldToNewOutputs,
- corDefOutputs);
- }
-
- /** Finds a {@link RexInputRef} that is equivalent to a {@link CorRef},
- * and if found, throws a {@link Util.FoundOne}. */
- private void findCorrelationEquivalent(CorRef correlation, RexNode e)
- throws Util.FoundOne {
- switch (e.getKind()) {
- case EQUALS:
- final RexCall call = (RexCall) e;
- final List<RexNode> operands = call.getOperands();
- if (references(operands.get(0), correlation)) {
- throw new Util.FoundOne(operands.get(1));
- }
- if (references(operands.get(1), correlation)) {
- throw new Util.FoundOne(operands.get(0));
- }
- break;
- case AND:
- for (RexNode operand : ((RexCall) e).getOperands()) {
- findCorrelationEquivalent(correlation, operand);
- }
- }
- }
-
- private boolean references(RexNode e, CorRef correlation) {
- switch (e.getKind()) {
- case CAST:
- final RexNode operand = ((RexCall) e).getOperands().get(0);
- if (isWidening(e.getType(), operand.getType())) {
- return references(operand, correlation);
- }
- return false;
- case FIELD_ACCESS:
- final RexFieldAccess f = (RexFieldAccess) e;
- if (f.getField().getIndex() == correlation.field
- && f.getReferenceExpr() instanceof RexCorrelVariable) {
- if (((RexCorrelVariable) f.getReferenceExpr()).id == correlation.corr) {
- return true;
- }
- }
- // fall through
- default:
- return false;
- }
- }
-
- /** Returns whether one type is just a widening of another.
- *
- * <p>For example:<ul>
- * <li>{@code VARCHAR(10)} is a widening of {@code VARCHAR(5)}.
- * <li>{@code VARCHAR(10)} is a widening of {@code VARCHAR(10) NOT NULL}.
- * </ul>
- */
- private boolean isWidening(RelDataType type, RelDataType type1) {
- return type.getSqlTypeName() == type1.getSqlTypeName()
- && type.getPrecision() >= type1.getPrecision();
- }
-
- /**
- * Rewrite LogicalSnapshot.
- *
- * @param rel the snapshot rel to rewrite
- */
- public Frame decorrelateRel(LogicalSnapshot rel) {
- if (RexUtil.containsCorrelation(rel.getPeriod())) {
- return null;
- }
- return decorrelateRel((RelNode) rel);
- }
-
- /**
- * Rewrite LogicalFilter.
- *
- * @param rel the filter rel to rewrite
- */
- public Frame decorrelateRel(LogicalFilter rel) {
- //
- // Rewrite logic:
- //
- // 1. If a Filter references a correlated field in its filter
- // condition, rewrite the Filter to be
- // Filter
- // Join(cross product)
- // originalFilterInput
- // ValueGenerator(produces distinct sets of correlated variables)
- // and rewrite the correlated fieldAccess in the filter condition to
- // reference the Join output.
- //
- // 2. If Filter does not reference correlated variables, simply
- // rewrite the filter condition using new input.
- //
-
- final RelNode oldInput = rel.getInput();
- Frame frame = getInvoke(oldInput, rel);
- if (frame == null) {
- // If input has not been rewritten, do not rewrite this rel.
- return null;
- }
-
- // If this Filter has correlated reference, create value generator
- // and produce the correlated variables in the new output.
- if (false) {
- if (cm.mapRefRelToCorRef.containsKey(rel)) {
- frame = decorrelateInputWithValueGenerator(rel, frame);
- }
- } else {
- frame = maybeAddValueGenerator(rel, frame);
- }
-
- final CorelMap cm2 = new CorelMapBuilder().build(rel);
-
- // Replace the filter expression to reference output of the join
- // Map filter to the new filter over join
- relBuilder.push(frame.r)
- .filter(decorrelateExpr(currentRel, map, cm2, rel.getCondition()));
-
- // Filter does not change the input ordering.
- // Filter rel does not permute the input.
- // All corVars produced by filter will have the same output positions in the
- // input rel.
- return register(rel, relBuilder.build(), frame.oldToNewOutputs,
- frame.corDefOutputs);
- }
-
- /**
- * Rewrite Correlate into a left outer join.
- *
- * @param rel Correlator
- */
- public Frame decorrelateRel(LogicalCorrelate rel) {
- //
- // Rewrite logic:
- //
- // The original left input will be joined with the new right input that
- // has generated correlated variables propagated up. For any generated
- // corVars that are not used in the join key, pass them along to be
- // joined later with the Correlates that produce them.
- //
-
- // the right input to Correlate should produce correlated variables
- final RelNode oldLeft = rel.getInput(0);
- final RelNode oldRight = rel.getInput(1);
-
- final Frame leftFrame = getInvoke(oldLeft, rel);
- final Frame rightFrame = getInvoke(oldRight, rel);
-
- if (leftFrame == null || rightFrame == null) {
- // If any input has not been rewritten, do not rewrite this rel.
- return null;
- }
-
- if (rightFrame.corDefOutputs.isEmpty()) {
- return null;
- }
-
- assert rel.getRequiredColumns().cardinality()
- <= rightFrame.corDefOutputs.keySet().size();
-
- // Change correlator rel into a join.
- // Join all the correlated variables produced by this correlator rel
- // with the values generated and propagated from the right input
- final SortedMap<CorDef, Integer> corDefOutputs =
- new TreeMap<>(rightFrame.corDefOutputs);
- final List<RexNode> conditions = new ArrayList<>();
- final List<RelDataTypeField> newLeftOutput =
- leftFrame.r.getRowType().getFieldList();
- int newLeftFieldCount = newLeftOutput.size();
-
- final List<RelDataTypeField> newRightOutput =
- rightFrame.r.getRowType().getFieldList();
-
- for (Map.Entry<CorDef, Integer> rightOutput
- : new ArrayList<>(corDefOutputs.entrySet())) {
- final CorDef corDef = rightOutput.getKey();
- if (!corDef.corr.equals(rel.getCorrelationId())) {
- continue;
- }
- final int newLeftPos = leftFrame.oldToNewOutputs.get(corDef.field);
- final int newRightPos = rightOutput.getValue();
- conditions.add(
- relBuilder.call(SqlStdOperatorTable.EQUALS,
- RexInputRef.of(newLeftPos, newLeftOutput),
- new RexInputRef(newLeftFieldCount + newRightPos,
- newRightOutput.get(newRightPos).getType())));
-
- // remove this corVar from output position mapping
- corDefOutputs.remove(corDef);
- }
-
- // Update the output position for the corVars: only pass on the cor
- // vars that are not used in the join key.
- for (CorDef corDef : corDefOutputs.keySet()) {
- int newPos = corDefOutputs.get(corDef) + newLeftFieldCount;
- corDefOutputs.put(corDef, newPos);
- }
-
- // then add any corVar from the left input. Do not need to change
- // output positions.
- corDefOutputs.putAll(leftFrame.corDefOutputs);
-
- // Create the mapping between the output of the old correlation rel
- // and the new join rel
- final Map<Integer, Integer> mapOldToNewOutputs = new HashMap<>();
-
- int oldLeftFieldCount = oldLeft.getRowType().getFieldCount();
-
- int oldRightFieldCount = oldRight.getRowType().getFieldCount();
- //noinspection AssertWithSideEffects
- assert rel.getRowType().getFieldCount()
- == oldLeftFieldCount + oldRightFieldCount;
-
- // Left input positions are not changed.
- mapOldToNewOutputs.putAll(leftFrame.oldToNewOutputs);
-
- // Right input positions are shifted by newLeftFieldCount.
- for (int i = 0; i < oldRightFieldCount; i++) {
- mapOldToNewOutputs.put(i + oldLeftFieldCount,
- rightFrame.oldToNewOutputs.get(i) + newLeftFieldCount);
- }
-
- final RexNode condition =
- RexUtil.composeConjunction(relBuilder.getRexBuilder(), conditions);
- RelNode newJoin =
- LogicalJoin.create(leftFrame.r, rightFrame.r, condition,
- ImmutableSet.of(), rel.getJoinType());
-
- return register(rel, newJoin, mapOldToNewOutputs, corDefOutputs);
- }
-
- /**
- * Rewrite LogicalJoin.
- *
- * @param rel Join
- */
- public Frame decorrelateRel(LogicalJoin rel) {
- // For SEMI/ANTI join decorrelate it's input directly,
- // because the correlate variables can only be propagated from
- // the left side, which is not supported yet.
- if (!rel.getJoinType().projectsRight()) {
- // fix CALCITE-3169
- return decorrelateRel((RelNode) rel);
- }
-
- //
- // Rewrite logic:
- //
- // 1. rewrite join condition.
- // 2. map output positions and produce corVars if any.
- //
-
- final RelNode oldLeft = rel.getInput(0);
- final RelNode oldRight = rel.getInput(1);
-
- final Frame leftFrame = getInvoke(oldLeft, rel);
- final Frame rightFrame = getInvoke(oldRight, rel);
-
- if (leftFrame == null || rightFrame == null) {
- // If any input has not been rewritten, do not rewrite this rel.
- return null;
- }
-
- final RelNode newJoin =
- LogicalJoin.create(leftFrame.r, rightFrame.r,
- decorrelateExpr(currentRel, map, cm, rel.getCondition()),
- ImmutableSet.of(), rel.getJoinType());
-
- // Create the mapping between the output of the old correlation rel
- // and the new join rel
- Map<Integer, Integer> mapOldToNewOutputs = new HashMap<>();
-
- int oldLeftFieldCount = oldLeft.getRowType().getFieldCount();
- int newLeftFieldCount = leftFrame.r.getRowType().getFieldCount();
-
- int oldRightFieldCount = oldRight.getRowType().getFieldCount();
- //noinspection AssertWithSideEffects
- assert rel.getRowType().getFieldCount()
- == oldLeftFieldCount + oldRightFieldCount;
-
- // Left input positions are not changed.
- mapOldToNewOutputs.putAll(leftFrame.oldToNewOutputs);
-
- // Right input positions are shifted by newLeftFieldCount.
- for (int i = 0; i < oldRightFieldCount; i++) {
- mapOldToNewOutputs.put(i + oldLeftFieldCount,
- rightFrame.oldToNewOutputs.get(i) + newLeftFieldCount);
- }
-
- final SortedMap<CorDef, Integer> corDefOutputs =
- new TreeMap<>(leftFrame.corDefOutputs);
-
- // Right input positions are shifted by newLeftFieldCount.
- for (Map.Entry<CorDef, Integer> entry
- : rightFrame.corDefOutputs.entrySet()) {
- corDefOutputs.put(entry.getKey(),
- entry.getValue() + newLeftFieldCount);
- }
- return register(rel, newJoin, mapOldToNewOutputs, corDefOutputs);
- }
-
- private static RexInputRef getNewForOldInputRef(RelNode currentRel,
- Map<RelNode, Frame> map, RexInputRef oldInputRef) {
- assert currentRel != null;
-
- int oldOrdinal = oldInputRef.getIndex();
- int newOrdinal = 0;
-
- // determine which input rel oldOrdinal references, and adjust
- // oldOrdinal to be relative to that input rel
- RelNode oldInput = null;
-
- for (RelNode oldInput0 : currentRel.getInputs()) {
- RelDataType oldInputType = oldInput0.getRowType();
- int n = oldInputType.getFieldCount();
- if (oldOrdinal < n) {
- oldInput = oldInput0;
- break;
- }
- RelNode newInput = map.get(oldInput0).r;
- newOrdinal += newInput.getRowType().getFieldCount();
- oldOrdinal -= n;
- }
-
- assert oldInput != null;
-
- final Frame frame = map.get(oldInput);
- assert frame != null;
-
- // now oldOrdinal is relative to oldInput
- int oldLocalOrdinal = oldOrdinal;
-
- // figure out the newLocalOrdinal, relative to the newInput.
- int newLocalOrdinal = oldLocalOrdinal;
-
- if (!frame.oldToNewOutputs.isEmpty()) {
- newLocalOrdinal = frame.oldToNewOutputs.get(oldLocalOrdinal);
- }
-
- newOrdinal += newLocalOrdinal;
-
- return new RexInputRef(newOrdinal,
- frame.r.getRowType().getFieldList().get(newLocalOrdinal).getType());
- }
-
- /**
- * Pulls project above the join from its RHS input. Enforces nullability
- * for join output.
- *
- * @param join Join
- * @param project Original project as the right-hand input of the join
- * @param nullIndicatorPos Position of null indicator
- * @return the subtree with the new Project at the root
- */
- private RelNode projectJoinOutputWithNullability(
- LogicalJoin join,
- LogicalProject project,
- int nullIndicatorPos) {
- final RelDataTypeFactory typeFactory = join.getCluster().getTypeFactory();
- final RelNode left = join.getLeft();
- final JoinRelType joinType = join.getJoinType();
-
- RexInputRef nullIndicator =
- new RexInputRef(
- nullIndicatorPos,
- typeFactory.createTypeWithNullability(
- join.getRowType().getFieldList().get(nullIndicatorPos)
- .getType(),
- true));
-
- // now create the new project
- List<Pair<RexNode, String>> newProjExprs = new ArrayList<>();
-
- // project everything from the LHS and then those from the original
- // projRel
- List<RelDataTypeField> leftInputFields =
- left.getRowType().getFieldList();
-
- for (int i = 0; i < leftInputFields.size(); i++) {
- newProjExprs.add(RexInputRef.of2(i, leftInputFields));
- }
-
- // Marked where the projected expr is coming from so that the types will
- // become nullable for the original projections which are now coming out
- // of the nullable side of the OJ.
- boolean projectPulledAboveLeftCorrelator =
- joinType.generatesNullsOnRight();
-
- for (Pair<RexNode, String> pair : project.getNamedProjects()) {
- RexNode newProjExpr =
- removeCorrelationExpr(
- pair.left,
- projectPulledAboveLeftCorrelator,
- nullIndicator);
-
- newProjExprs.add(Pair.of(newProjExpr, pair.right));
- }
-
- return relBuilder.push(join)
- .projectNamed(Pair.left(newProjExprs), Pair.right(newProjExprs), true)
- .build();
- }
-
- /**
- * Pulls a {@link Project} above a {@link Correlate} from its RHS input.
- * Enforces nullability for join output.
- *
- * @param correlate Correlate
- * @param project the original project as the RHS input of the join
- * @param isCount Positions which are calls to the <code>COUNT</code>
- * aggregation function
- * @return the subtree with the new Project at the root
- */
- private RelNode aggregateCorrelatorOutput(
- Correlate correlate,
- LogicalProject project,
- Set<Integer> isCount) {
- final RelNode left = correlate.getLeft();
- final JoinRelType joinType = correlate.getJoinType();
-
- // now create the new project
- final List<Pair<RexNode, String>> newProjects = new ArrayList<>();
-
- // Project everything from the LHS and then those from the original
- // project
- final List<RelDataTypeField> leftInputFields =
- left.getRowType().getFieldList();
-
- for (int i = 0; i < leftInputFields.size(); i++) {
- newProjects.add(RexInputRef.of2(i, leftInputFields));
- }
-
- // Marked where the projected expr is coming from so that the types will
- // become nullable for the original projections which are now coming out
- // of the nullable side of the OJ.
- boolean projectPulledAboveLeftCorrelator =
- joinType.generatesNullsOnRight();
-
- for (Pair<RexNode, String> pair : project.getNamedProjects()) {
- RexNode newProjExpr =
- removeCorrelationExpr(
- pair.left,
- projectPulledAboveLeftCorrelator,
- isCount);
- newProjects.add(Pair.of(newProjExpr, pair.right));
- }
-
- return relBuilder.push(correlate)
- .projectNamed(Pair.left(newProjects), Pair.right(newProjects), true)
- .build();
- }
-
- /**
- * Checks whether the correlations in projRel and filter are related to
- * the correlated variables provided by corRel.
- *
- * @param correlate Correlate
- * @param project The original Project as the RHS input of the join
- * @param filter Filter
- * @param correlatedJoinKeys Correlated join keys
- * @return true if filter and proj only references corVar provided by corRel
- */
- private boolean checkCorVars(
- LogicalCorrelate correlate,
- LogicalProject project,
- LogicalFilter filter,
- List<RexFieldAccess> correlatedJoinKeys) {
- if (filter != null) {
- assert correlatedJoinKeys != null;
-
- // check that all correlated refs in the filter condition are
- // used in the join(as field access).
- Set<CorRef> corVarInFilter =
- Sets.newHashSet(cm.mapRefRelToCorRef.get(filter));
-
- for (RexFieldAccess correlatedJoinKey : correlatedJoinKeys) {
- corVarInFilter.remove(cm.mapFieldAccessToCorRef.get(correlatedJoinKey));
- }
-
- if (!corVarInFilter.isEmpty()) {
- return false;
- }
-
- // Check that the correlated variables referenced in these
- // comparisons do come from the Correlate.
- corVarInFilter.addAll(cm.mapRefRelToCorRef.get(filter));
-
- for (CorRef corVar : corVarInFilter) {
- if (cm.mapCorToCorRel.get(corVar.corr) != correlate) {
- return false;
- }
- }
- }
-
- // if project has any correlated reference, make sure they are also
- // provided by the current correlate. They will be projected out of the LHS
- // of the correlate.
- if ((project != null) && cm.mapRefRelToCorRef.containsKey(project)) {
- for (CorRef corVar : cm.mapRefRelToCorRef.get(project)) {
- if (cm.mapCorToCorRel.get(corVar.corr) != correlate) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Remove correlated variables from the tree at root corRel
- *
- * @param correlate Correlate
- */
- private void removeCorVarFromTree(LogicalCorrelate correlate) {
- if (cm.mapCorToCorRel.get(correlate.getCorrelationId()) == correlate) {
- cm.mapCorToCorRel.remove(correlate.getCorrelationId());
- }
- }
-
- /**
- * Projects all {@code input} output fields plus the additional expressions.
- *
- * @param input Input relational expression
- * @param additionalExprs Additional expressions and names
- * @return the new Project
- */
- private RelNode createProjectWithAdditionalExprs(
- RelNode input,
- List<Pair<RexNode, String>> additionalExprs) {
- final List<RelDataTypeField> fieldList =
- input.getRowType().getFieldList();
- List<Pair<RexNode, String>> projects = new ArrayList<>();
- for (Ord<RelDataTypeField> field : Ord.zip(fieldList)) {
- projects.add(
- Pair.of(
- (RexNode) relBuilder.getRexBuilder().makeInputRef(
- field.e.getType(), field.i),
- field.e.getName()));
- }
- projects.addAll(additionalExprs);
- return relBuilder.push(input)
- .projectNamed(Pair.left(projects), Pair.right(projects), true)
- .build();
- }
-
- /* Returns an immutable map with the identity [0: 0, .., count-1: count-1]. */
- static Map<Integer, Integer> identityMap(int count) {
- ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
- for (int i = 0; i < count; i++) {
- builder.put(i, i);
- }
- return builder.build();
- }
-
- /** Registers a relational expression and the relational expression it became
- * after decorrelation. */
- Frame register(RelNode rel, RelNode newRel,
- Map<Integer, Integer> oldToNewOutputs,
- SortedMap<CorDef, Integer> corDefOutputs) {
- final Frame frame = new Frame(rel, newRel, corDefOutputs, oldToNewOutputs);
- map.put(rel, frame);
- return frame;
- }
-
- static boolean allLessThan(Collection<Integer> integers, int limit,
- Litmus ret) {
- for (int value : integers) {
- if (value >= limit) {
- return ret.fail("out of range; value: {}, limit: {}", value, limit);
- }
- }
- return ret.succeed();
- }
-
- private static RelNode stripHep(RelNode rel) {
- if (rel instanceof HepRelVertex) {
- HepRelVertex hepRelVertex = (HepRelVertex) rel;
- rel = hepRelVertex.getCurrentRel();
- }
- return rel;
- }
-
- //~ Inner Classes ----------------------------------------------------------
-
- /** Shuttle that decorrelates. */
- private static class DecorrelateRexShuttle extends RexShuttle {
- private final RelNode currentRel;
- private final Map<RelNode, Frame> map;
- private final CorelMap cm;
-
- private DecorrelateRexShuttle(RelNode currentRel,
- Map<RelNode, Frame> map, CorelMap cm) {
- this.currentRel = Objects.requireNonNull(currentRel);
- this.map = Objects.requireNonNull(map);
- this.cm = Objects.requireNonNull(cm);
- }
-
- @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
- int newInputOutputOffset = 0;
- for (RelNode input : currentRel.getInputs()) {
- final Frame frame = map.get(input);
-
- if (frame != null) {
- // try to find in this input rel the position of corVar
- final CorRef corRef = cm.mapFieldAccessToCorRef.get(fieldAccess);
-
- if (corRef != null) {
- Integer newInputPos = frame.corDefOutputs.get(corRef.def());
- if (newInputPos != null) {
- // This input does produce the corVar referenced.
- return new RexInputRef(newInputPos + newInputOutputOffset,
- frame.r.getRowType().getFieldList().get(newInputPos)
- .getType());
- }
- }
-
- // this input does not produce the corVar needed
- newInputOutputOffset += frame.r.getRowType().getFieldCount();
- } else {
- // this input is not rewritten
- newInputOutputOffset += input.getRowType().getFieldCount();
- }
- }
- return fieldAccess;
- }
-
- @Override public RexNode visitInputRef(RexInputRef inputRef) {
- final RexInputRef ref = getNewForOldInputRef(currentRel, map, inputRef);
- if (ref.getIndex() == inputRef.getIndex()
- && ref.getType() == inputRef.getType()) {
- return inputRef; // re-use old object, to prevent needless expr cloning
- }
- return ref;
- }
- }
-
- /** Shuttle that removes correlations. */
- private class RemoveCorrelationRexShuttle extends RexShuttle {
- final RexBuilder rexBuilder;
- final RelDataTypeFactory typeFactory;
- final boolean projectPulledAboveLeftCorrelator;
- final RexInputRef nullIndicator;
- final ImmutableSet<Integer> isCount;
-
- RemoveCorrelationRexShuttle(
- RexBuilder rexBuilder,
- boolean projectPulledAboveLeftCorrelator,
- RexInputRef nullIndicator,
- Set<Integer> isCount) {
- this.projectPulledAboveLeftCorrelator =
- projectPulledAboveLeftCorrelator;
- this.nullIndicator = nullIndicator; // may be null
- this.isCount = ImmutableSet.copyOf(isCount);
- this.rexBuilder = rexBuilder;
- this.typeFactory = rexBuilder.getTypeFactory();
- }
-
- private RexNode createCaseExpression(
- RexInputRef nullInputRef,
- RexLiteral lit,
- RexNode rexNode) {
- RexNode[] caseOperands = new RexNode[3];
-
- // Construct a CASE expression to handle the null indicator.
- //
- // This also covers the case where a left correlated sub-query
- // projects fields from outer relation. Since LOJ cannot produce
- // nulls on the LHS, the projection now need to make a nullable LHS
- // reference using a nullability indicator. If this this indicator
- // is null, it means the sub-query does not produce any value. As a
- // result, any RHS ref by this usbquery needs to produce null value.
-
- // WHEN indicator IS NULL
- caseOperands[0] =
- rexBuilder.makeCall(
- SqlStdOperatorTable.IS_NULL,
- new RexInputRef(
- nullInputRef.getIndex(),
- typeFactory.createTypeWithNullability(
- nullInputRef.getType(),
- true)));
-
- // THEN CAST(NULL AS newInputTypeNullable)
- caseOperands[1] =
- rexBuilder.makeCast(
- typeFactory.createTypeWithNullability(
- rexNode.getType(),
- true),
- lit);
-
- // ELSE cast (newInput AS newInputTypeNullable) END
- caseOperands[2] =
- rexBuilder.makeCast(
- typeFactory.createTypeWithNullability(
- rexNode.getType(),
- true),
- rexNode);
-
- return rexBuilder.makeCall(
- SqlStdOperatorTable.CASE,
- caseOperands);
- }
-
- @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
- if (cm.mapFieldAccessToCorRef.containsKey(fieldAccess)) {
- // if it is a corVar, change it to be input ref.
- CorRef corVar = cm.mapFieldAccessToCorRef.get(fieldAccess);
-
- // corVar offset should point to the leftInput of currentRel,
- // which is the Correlate.
- RexNode newRexNode =
- new RexInputRef(corVar.field, fieldAccess.getType());
-
- if (projectPulledAboveLeftCorrelator
- && (nullIndicator != null)) {
- // need to enforce nullability by applying an additional
- // cast operator over the transformed expression.
- newRexNode =
- createCaseExpression(
- nullIndicator,
- rexBuilder.constantNull(),
- newRexNode);
- }
- return newRexNode;
- }
- return fieldAccess;
- }
-
- @Override public RexNode visitInputRef(RexInputRef inputRef) {
- if (currentRel instanceof LogicalCorrelate) {
- // if this rel references corVar
- // and now it needs to be rewritten
- // it must have been pulled above the Correlate
- // replace the input ref to account for the LHS of the
- // Correlate
- final int leftInputFieldCount =
- ((LogicalCorrelate) currentRel).getLeft().getRowType()
- .getFieldCount();
- RelDataType newType = inputRef.getType();
-
- if (projectPulledAboveLeftCorrelator) {
- newType =
- typeFactory.createTypeWithNullability(newType, true);
- }
-
- int pos = inputRef.getIndex();
- RexInputRef newInputRef =
- new RexInputRef(leftInputFieldCount + pos, newType);
-
- if ((isCount != null) && isCount.contains(pos)) {
- return createCaseExpression(
- newInputRef,
- rexBuilder.makeExactLiteral(BigDecimal.ZERO),
- newInputRef);
- } else {
- return newInputRef;
- }
- }
- return inputRef;
- }
-
- @Override public RexNode visitLiteral(RexLiteral literal) {
- // Use nullIndicator to decide whether to project null.
- // Do nothing if the literal is null.
- if (!RexUtil.isNull(literal)
- && projectPulledAboveLeftCorrelator
- && (nullIndicator != null)) {
- return createCaseExpression(
- nullIndicator,
- rexBuilder.constantNull(),
- literal);
- }
- return literal;
- }
-
- @Override public RexNode visitCall(final RexCall call) {
- RexNode newCall;
-
- boolean[] update = {false};
- List<RexNode> clonedOperands = visitList(call.operands, update);
- if (update[0]) {
- SqlOperator operator = call.getOperator();
-
- boolean isSpecialCast = false;
- if (operator instanceof SqlFunction) {
- SqlFunction function = (SqlFunction) operator;
- if (function.getKind() == SqlKind.CAST) {
- if (call.operands.size() < 2) {
- isSpecialCast = true;
- }
- }
- }
-
- final RelDataType newType;
- if (!isSpecialCast) {
- // TODO: ideally this only needs to be called if the result
- // type will also change. However, since that requires
- // support from type inference rules to tell whether a rule
- // decides return type based on input types, for now all
- // operators will be recreated with new type if any operand
- // changed, unless the operator has "built-in" type.
- newType = rexBuilder.deriveReturnType(operator, clonedOperands);
- } else {
- // Use the current return type when creating a new call, for
- // operators with return type built into the operator
- // definition, and with no type inference rules, such as
- // cast function with less than 2 operands.
-
- // TODO: Comments in RexShuttle.visitCall() mention other
- // types in this category. Need to resolve those together
- // and preferably in the base class RexShuttle.
- newType = call.getType();
- }
- newCall =
- rexBuilder.makeCall(
- newType,
- operator,
- clonedOperands);
- } else {
- newCall = call;
- }
-
- if (projectPulledAboveLeftCorrelator && (nullIndicator != null)) {
- return createCaseExpression(
- nullIndicator,
- rexBuilder.constantNull(),
- newCall);
- }
- return newCall;
- }
- }
-
- /**
- * Rule to remove single_value rel. For cases like
- *
- * <blockquote>AggRel single_value proj/filter/agg/ join on unique LHS key
- * AggRel single group</blockquote>
- */
- private final class RemoveSingleAggregateRule extends RelOptRule {
- RemoveSingleAggregateRule(RelBuilderFactory relBuilderFactory) {
- super(
- operand(
- LogicalAggregate.class,
- operand(
- LogicalProject.class,
- operand(LogicalAggregate.class, any()))),
- relBuilderFactory, null);
- }
-
- public void onMatch(RelOptRuleCall call) {
- LogicalAggregate singleAggregate = call.rel(0);
- LogicalProject project = call.rel(1);
- LogicalAggregate aggregate = call.rel(2);
-
- // check singleAggRel is single_value agg
- if ((!singleAggregate.getGroupSet().isEmpty())
- || (singleAggregate.getAggCallList().size() != 1)
- || !(singleAggregate.getAggCallList().get(0).getAggregation()
- instanceof SqlSingleValueAggFunction)) {
- return;
- }
-
- // check projRel only projects one expression
- // check this project only projects one expression, i.e. scalar
- // sub-queries.
- List<RexNode> projExprs = project.getProjects();
- if (projExprs.size() != 1) {
- return;
- }
-
- // check the input to project is an aggregate on the entire input
- if (!aggregate.getGroupSet().isEmpty()) {
- return;
- }
-
- // singleAggRel produces a nullable type, so create the new
- // projection that casts proj expr to a nullable type.
- final RelBuilder relBuilder = call.builder();
- final RelDataType type =
- relBuilder.getTypeFactory()
- .createTypeWithNullability(projExprs.get(0).getType(), true);
- final RexNode cast =
- relBuilder.getRexBuilder().makeCast(type, projExprs.get(0));
- relBuilder.push(aggregate)
- .project(cast);
- call.transformTo(relBuilder.build());
- }
- }
-
- /** Planner rule that removes correlations for scalar projects. */
- private final class RemoveCorrelationForScalarProjectRule extends RelOptRule {
- RemoveCorrelationForScalarProjectRule(RelBuilderFactory relBuilderFactory) {
- super(
- operand(LogicalCorrelate.class,
- operand(RelNode.class, any()),
- operand(LogicalAggregate.class,
- operand(LogicalProject.class,
- operand(RelNode.class, any())))),
- relBuilderFactory, null);
- }
-
- public void onMatch(RelOptRuleCall call) {
- final LogicalCorrelate correlate = call.rel(0);
- final RelNode left = call.rel(1);
- final LogicalAggregate aggregate = call.rel(2);
- final LogicalProject project = call.rel(3);
- RelNode right = call.rel(4);
- final RelOptCluster cluster = correlate.getCluster();
-
- setCurrent(call.getPlanner().getRoot(), correlate);
-
- // Check for this pattern.
- // The pattern matching could be simplified if rules can be applied
- // during decorrelation.
- //
- // Correlate(left correlation, condition = true)
- // leftInput
- // Aggregate (groupby (0) single_value())
- // Project-A (may reference corVar)
- // rightInput
- final JoinRelType joinType = correlate.getJoinType();
-
- // corRel.getCondition was here, however Correlate was updated so it
- // never includes a join condition. The code was not modified for brevity.
- RexNode joinCond = relBuilder.literal(true);
- if ((joinType != JoinRelType.LEFT)
- || (joinCond != relBuilder.literal(true))) {
- return;
- }
-
- // check that the agg is of the following type:
- // doing a single_value() on the entire input
- if ((!aggregate.getGroupSet().isEmpty())
- || (aggregate.getAggCallList().size() != 1)
- || !(aggregate.getAggCallList().get(0).getAggregation()
- instanceof SqlSingleValueAggFunction)) {
- return;
- }
-
- // check this project only projects one expression, i.e. scalar
- // sub-queries.
- if (project.getProjects().size() != 1) {
- return;
- }
-
- int nullIndicatorPos;
-
- if ((right instanceof LogicalFilter)
- && cm.mapRefRelToCorRef.containsKey(right)) {
- // rightInput has this shape:
- //
- // Filter (references corVar)
- // filterInput
-
- // If rightInput is a filter and contains correlated
- // reference, make sure the correlated keys in the filter
- // condition forms a unique key of the RHS.
-
- LogicalFilter filter = (LogicalFilter) right;
- right = filter.getInput();
-
- assert right instanceof HepRelVertex;
- right = ((HepRelVertex) right).getCurrentRel();
-
- // check filter input contains no correlation
- if (RelOptUtil.getVariablesUsed(right).size() > 0) {
- return;
- }
-
- // extract the correlation out of the filter
-
- // First breaking up the filter conditions into equality
- // comparisons between rightJoinKeys (from the original
- // filterInput) and correlatedJoinKeys. correlatedJoinKeys
- // can be expressions, while rightJoinKeys need to be input
- // refs. These comparisons are AND'ed together.
- List<RexNode> tmpRightJoinKeys = new ArrayList<>();
- List<RexNode> correlatedJoinKeys = new ArrayList<>();
- RelOptUtil.splitCorrelatedFilterCondition(
- filter,
- tmpRightJoinKeys,
- correlatedJoinKeys,
- false);
-
- // check that the columns referenced in these comparisons form
- // an unique key of the filterInput
- final List<RexInputRef> rightJoinKeys = new ArrayList<>();
- for (RexNode key : tmpRightJoinKeys) {
- assert key instanceof RexInputRef;
- rightJoinKeys.add((RexInputRef) key);
- }
-
- // check that the columns referenced in rightJoinKeys form an
- // unique key of the filterInput
- if (rightJoinKeys.isEmpty()) {
- return;
- }
-
- // The join filters out the nulls. So, it's ok if there are
- // nulls in the join keys.
- final RelMetadataQuery mq = call.getMetadataQuery();
- if (!RelMdUtil.areColumnsDefinitelyUniqueWhenNullsFiltered(mq, right,
- rightJoinKeys)) {
- SQL2REL_LOGGER.debug("{} are not unique keys for {}",
- rightJoinKeys.toString(), right.toString());
- return;
- }
-
- RexUtil.FieldAccessFinder visitor =
- new RexUtil.FieldAccessFinder();
- RexUtil.apply(visitor, correlatedJoinKeys, null);
- List<RexFieldAccess> correlatedKeyList =
- visitor.getFieldAccessList();
-
- if (!checkCorVars(correlate, project, filter, correlatedKeyList)) {
- return;
- }
-
- // Change the plan to this structure.
- // Note that the Aggregate is removed.
- //
- // Project-A' (replace corVar to input ref from the Join)
- // Join (replace corVar to input ref from leftInput)
- // leftInput
- // rightInput (previously filterInput)
-
- // Change the filter condition into a join condition
- joinCond =
- removeCorrelationExpr(filter.getCondition(), false);
-
- nullIndicatorPos =
- left.getRowType().getFieldCount()
- + rightJoinKeys.get(0).getIndex();
- } else if (cm.mapRefRelToCorRef.containsKey(project)) {
- // check filter input contains no correlation
- if (RelOptUtil.getVariablesUsed(right).size() > 0) {
- return;
- }
-
- if (!checkCorVars(correlate, project, null, null)) {
- return;
- }
-
- // Change the plan to this structure.
- //
- // Project-A' (replace corVar to input ref from Join)
- // Join (left, condition = true)
- // leftInput
- // Aggregate(groupby(0), single_value(0), s_v(1)....)
- // Project-B (everything from input plus literal true)
- // projectInput
-
- // make the new Project to provide a null indicator
- right =
- createProjectWithAdditionalExprs(right,
- ImmutableList.of(
- Pair.of(relBuilder.literal(true), "nullIndicator")));
-
- // make the new aggRel
- right =
- RelOptUtil.createSingleValueAggRel(cluster, right);
-
- // The last field:
- // single_value(true)
- // is the nullIndicator
- nullIndicatorPos =
- left.getRowType().getFieldCount()
- + right.getRowType().getFieldCount() - 1;
- } else {
- return;
- }
-
- // make the new join rel
- LogicalJoin join =
- LogicalJoin.create(left, right, joinCond,
- ImmutableSet.of(), joinType);
-
- RelNode newProject =
- projectJoinOutputWithNullability(join, project, nullIndicatorPos);
-
- call.transformTo(newProject);
-
- removeCorVarFromTree(correlate);
- }
- }
-
- /** Planner rule that removes correlations for scalar aggregates. */
- private final class RemoveCorrelationForScalarAggregateRule
- extends RelOptRule {
- RemoveCorrelationForScalarAggregateRule(RelBuilderFactory relBuilderFactory) {
- super(
- operand(LogicalCorrelate.class,
- operand(RelNode.class, any()),
- operand(LogicalProject.class,
- operandJ(LogicalAggregate.class, null, Aggregate::isSimple,
- operand(LogicalProject.class,
- operand(RelNode.class, any()))))),
- relBuilderFactory, null);
- }
-
- public void onMatch(RelOptRuleCall call) {
- final LogicalCorrelate correlate = call.rel(0);
- final RelNode left = call.rel(1);
- final LogicalProject aggOutputProject = call.rel(2);
- final LogicalAggregate aggregate = call.rel(3);
- final LogicalProject aggInputProject = call.rel(4);
- RelNode right = call.rel(5);
- final RelBuilder builder = call.builder();
- final RexBuilder rexBuilder = builder.getRexBuilder();
- final RelOptCluster cluster = correlate.getCluster();
-
- setCurrent(call.getPlanner().getRoot(), correlate);
-
- // check for this pattern
- // The pattern matching could be simplified if rules can be applied
- // during decorrelation,
- //
- // CorrelateRel(left correlation, condition = true)
- // leftInput
- // Project-A (a RexNode)
- // Aggregate (groupby (0), agg0(), agg1()...)
- // Project-B (references coVar)
- // rightInput
-
- // check aggOutputProject projects only one expression
- final List<RexNode> aggOutputProjects = aggOutputProject.getProjects();
- if (aggOutputProjects.size() != 1) {
- return;
- }
-
- final JoinRelType joinType = correlate.getJoinType();
- // corRel.getCondition was here, however Correlate was updated so it
- // never includes a join condition. The code was not modified for brevity.
- RexNode joinCond = rexBuilder.makeLiteral(true);
- if ((joinType != JoinRelType.LEFT)
- || (joinCond != rexBuilder.makeLiteral(true))) {
- return;
- }
-
- // check that the agg is on the entire input
- if (!aggregate.getGroupSet().isEmpty()) {
- return;
- }
-
- final List<RexNode> aggInputProjects = aggInputProject.getProjects();
-
- final List<AggregateCall> aggCalls = aggregate.getAggCallList();
- final Set<Integer> isCountStar = new HashSet<>();
-
- // mark if agg produces count(*) which needs to reference the
- // nullIndicator after the transformation.
- int k = -1;
- for (AggregateCall aggCall : aggCalls) {
- ++k;
- if ((aggCall.getAggregation() instanceof SqlCountAggFunction)
- && (aggCall.getArgList().size() == 0)) {
- isCountStar.add(k);
- }
- }
-
- if ((right instanceof LogicalFilter)
- && cm.mapRefRelToCorRef.containsKey(right)) {
- // rightInput has this shape:
- //
- // Filter (references corVar)
- // filterInput
- LogicalFilter filter = (LogicalFilter) right;
- right = filter.getInput();
-
- assert right instanceof HepRelVertex;
- right = ((HepRelVertex) right).getCurrentRel();
-
- // check filter input contains no correlation
- if (RelOptUtil.getVariablesUsed(right).size() > 0) {
- return;
- }
-
- // check filter condition type First extract the correlation out
- // of the filter
-
- // First breaking up the filter conditions into equality
- // comparisons between rightJoinKeys(from the original
- // filterInput) and correlatedJoinKeys. correlatedJoinKeys
- // can only be RexFieldAccess, while rightJoinKeys can be
- // expressions. These comparisons are AND'ed together.
- List<RexNode> rightJoinKeys = new ArrayList<>();
- List<RexNode> tmpCorrelatedJoinKeys = new ArrayList<>();
- RelOptUtil.splitCorrelatedFilterCondition(
- filter,
- rightJoinKeys,
- tmpCorrelatedJoinKeys,
- true);
-
- // make sure the correlated reference forms a unique key check
- // that the columns referenced in these comparisons form an
- // unique key of the leftInput
- List<RexFieldAccess> correlatedJoinKeys = new ArrayList<>();
- List<RexInputRef> correlatedInputRefJoinKeys = new ArrayList<>();
- for (RexNode joinKey : tmpCorrelatedJoinKeys) {
- assert joinKey instanceof RexFieldAccess;
- correlatedJoinKeys.add((RexFieldAccess) joinKey);
- RexNode correlatedInputRef =
- removeCorrelationExpr(joinKey, false);
- assert correlatedInputRef instanceof RexInputRef;
- correlatedInputRefJoinKeys.add(
- (RexInputRef) correlatedInputRef);
- }
-
- // check that the columns referenced in rightJoinKeys form an
- // unique key of the filterInput
- if (correlatedInputRefJoinKeys.isEmpty()) {
- return;
- }
-
- // The join filters out the nulls. So, it's ok if there are
- // nulls in the join keys.
- final RelMetadataQuery mq = call.getMetadataQuery();
- if (!RelMdUtil.areColumnsDefinitelyUniqueWhenNullsFiltered(mq, left,
- correlatedInputRefJoinKeys)) {
- SQL2REL_LOGGER.debug("{} are not unique keys for {}",
- correlatedJoinKeys.toString(), left.toString());
- return;
- }
-
- // check corVar references are valid
- if (!checkCorVars(correlate,
- aggInputProject,
- filter,
- correlatedJoinKeys)) {
- return;
- }
-
- // Rewrite the above plan:
- //
- // Correlate(left correlation, condition = true)
- // leftInput
- // Project-A (a RexNode)
- // Aggregate (groupby(0), agg0(),agg1()...)
- // Project-B (may reference corVar)
- // Filter (references corVar)
- // rightInput (no correlated reference)
- //
-
- // to this plan:
- //
- // Project-A' (all gby keys + rewritten nullable ProjExpr)
- // Aggregate (groupby(all left input refs)
- // agg0(rewritten expression),
- // agg1()...)
- // Project-B' (rewritten original projected exprs)
- // Join(replace corVar w/ input ref from leftInput)
- // leftInput
- // rightInput
- //
-
- // In the case where agg is count(*) or count($corVar), it is
- // changed to count(nullIndicator).
- // Note: any non-nullable field from the RHS can be used as
- // the indicator however a "true" field is added to the
- // projection list from the RHS for simplicity to avoid
- // searching for non-null fields.
- //
- // Project-A' (all gby keys + rewritten nullable ProjExpr)
- // Aggregate (groupby(all left input refs),
- // count(nullIndicator), other aggs...)
- // Project-B' (all left input refs plus
- // the rewritten original projected exprs)
- // Join(replace corVar to input ref from leftInput)
- // leftInput
- // Project (everything from rightInput plus
- // the nullIndicator "true")
- // rightInput
- //
-
- // first change the filter condition into a join condition
- joinCond =
- removeCorrelationExpr(filter.getCondition(), false);
- } else if (cm.mapRefRelToCorRef.containsKey(aggInputProject)) {
- // check rightInput contains no correlation
- if (RelOptUtil.getVariablesUsed(right).size() > 0) {
- return;
- }
-
- // check corVar references are valid
- if (!checkCorVars(correlate, aggInputProject, null, null)) {
- return;
- }
-
- int nFields = left.getRowType().getFieldCount();
- ImmutableBitSet allCols = ImmutableBitSet.range(nFields);
-
- // leftInput contains unique keys
- // i.e. each row is distinct and can group by on all the left
- // fields
- final RelMetadataQuery mq = call.getMetadataQuery();
- if (!RelMdUtil.areColumnsDefinitelyUnique(mq, left, allCols)) {
- SQL2REL_LOGGER.debug("There are no unique keys for {}", left);
- return;
- }
- //
- // Rewrite the above plan:
- //
- // CorrelateRel(left correlation, condition = true)
- // leftInput
- // Project-A (a RexNode)
- // Aggregate (groupby(0), agg0(), agg1()...)
- // Project-B (references coVar)
- // rightInput (no correlated reference)
- //
-
- // to this plan:
- //
- // Project-A' (all gby keys + rewritten nullable ProjExpr)
- // Aggregate (groupby(all left input refs)
- // agg0(rewritten expression),
- // agg1()...)
- // Project-B' (rewritten original projected exprs)
- // Join (LOJ cond = true)
- // leftInput
- // rightInput
- //
-
- // In the case where agg is count($corVar), it is changed to
- // count(nullIndicator).
- // Note: any non-nullable field from the RHS can be used as
- // the indicator however a "true" field is added to the
- // projection list from the RHS for simplicity to avoid
- // searching for non-null fields.
- //
- // Project-A' (all gby keys + rewritten nullable ProjExpr)
- // Aggregate (groupby(all left input refs),
- // count(nullIndicator), other aggs...)
- // Project-B' (all left input refs plus
- // the rewritten original projected exprs)
- // Join (replace corVar to input ref from leftInput)
- // leftInput
- // Project (everything from rightInput plus
- // the nullIndicator "true")
- // rightInput
- } else {
- return;
- }
-
- RelDataType leftInputFieldType = left.getRowType();
- int leftInputFieldCount = leftInputFieldType.getFieldCount();
- int joinOutputProjExprCount =
- leftInputFieldCount + aggInputProjects.size() + 1;
-
- right =
- createProjectWithAdditionalExprs(right,
- ImmutableList.of(
- Pair.of(rexBuilder.makeLiteral(true),
- "nullIndicator")));
-
- LogicalJoin join =
- LogicalJoin.create(left, right, joinCond,
- ImmutableSet.of(), joinType);
-
- // To the consumer of joinOutputProjRel, nullIndicator is located
- // at the end
- int nullIndicatorPos = join.getRowType().getFieldCount() - 1;
-
- RexInputRef nullIndicator =
- new RexInputRef(
- nullIndicatorPos,
- cluster.getTypeFactory().createTypeWithNullability(
- join.getRowType().getFieldList()
- .get(nullIndicatorPos).getType(),
- true));
-
- // first project all group-by keys plus the transformed agg input
- List<RexNode> joinOutputProjects = new ArrayList<>();
-
- // LOJ Join preserves LHS types
- for (int i = 0; i < leftInputFieldCount; i++) {
- joinOutputProjects.add(
- rexBuilder.makeInputRef(
- leftInputFieldType.getFieldList().get(i).getType(), i));
- }
-
- for (RexNode aggInputProjExpr : aggInputProjects) {
- joinOutputProjects.add(
- removeCorrelationExpr(aggInputProjExpr,
- joinType.generatesNullsOnRight(),
- nullIndicator));
- }
-
- joinOutputProjects.add(
- rexBuilder.makeInputRef(join, nullIndicatorPos));
-
- final RelNode joinOutputProject = builder.push(join)
- .project(joinOutputProjects)
- .build();
-
- // nullIndicator is now at a different location in the output of
- // the join
- nullIndicatorPos = joinOutputProjExprCount - 1;
-
- final int groupCount = leftInputFieldCount;
-
- List<AggregateCall> newAggCalls = new ArrayList<>();
- k = -1;
- for (AggregateCall aggCall : aggCalls) {
- ++k;
- final List<Integer> argList;
-
- if (isCountStar.contains(k)) {
- // this is a count(*), transform it to count(nullIndicator)
- // the null indicator is located at the end
- argList = Collections.singletonList(nullIndicatorPos);
- } else {
- argList = new ArrayList<>();
-
- for (int aggArg : aggCall.getArgList()) {
- argList.add(aggArg + groupCount);
- }
- }
-
- int filterArg = aggCall.filterArg < 0 ? aggCall.filterArg
- : aggCall.filterArg + groupCount;
- newAggCalls.add(
- aggCall.adaptTo(joinOutputProject, argList, filterArg,
- aggregate.getGroupCount(), groupCount));
- }
-
- ImmutableBitSet groupSet =
- ImmutableBitSet.range(groupCount);
- LogicalAggregate newAggregate =
- LogicalAggregate.create(joinOutputProject, groupSet, null,
- newAggCalls);
- List<RexNode> newAggOutputProjectList = new ArrayList<>();
- for (int i : groupSet) {
- newAggOutputProjectList.add(
- rexBuilder.makeInputRef(newAggregate, i));
- }
-
- RexNode newAggOutputProjects =
- removeCorrelationExpr(aggOutputProjects.get(0), false);
- newAggOutputProjectList.add(
- rexBuilder.makeCast(
- cluster.getTypeFactory().createTypeWithNullability(
- newAggOutputProjects.getType(),
- true),
- newAggOutputProjects));
-
- builder.push(newAggregate)
- .project(newAggOutputProjectList);
- call.transformTo(builder.build());
-
- removeCorVarFromTree(correlate);
- }
- }
-
- // REVIEW jhyde 29-Oct-2007: This rule is non-static, depends on the state
- // of members in RelDecorrelator, and has side-effects in the decorrelator.
- // This breaks the contract of a planner rule, and the rule will not be
- // reusable in other planners.
-
- // REVIEW jvs 29-Oct-2007: Shouldn't it also be incorporating
- // the flavor attribute into the description?
-
- /** Planner rule that adjusts projects when counts are added. */
- private final class AdjustProjectForCountAggregateRule extends RelOptRule {
- final boolean flavor;
-
- AdjustProjectForCountAggregateRule(boolean flavor,
- RelBuilderFactory relBuilderFactory) {
- super(
- flavor
- ? operand(LogicalCorrelate.class,
- operand(RelNode.class, any()),
- operand(LogicalProject.class,
- operand(LogicalAggregate.class, any())))
- : operand(LogicalCorrelate.class,
- operand(RelNode.class, any()),
- operand(LogicalAggregate.class, any())),
- relBuilderFactory, null);
- this.flavor = flavor;
- }
-
- public void onMatch(RelOptRuleCall call) {
- final LogicalCorrelate correlate = call.rel(0);
- final RelNode left = call.rel(1);
- final LogicalProject aggOutputProject;
- final LogicalAggregate aggregate;
- if (flavor) {
- aggOutputProject = call.rel(2);
- aggregate = call.rel(3);
- } else {
- aggregate = call.rel(2);
-
- // Create identity projection
- final List<Pair<RexNode, String>> projects = new ArrayList<>();
- final List<RelDataTypeField> fields =
- aggregate.getRowType().getFieldList();
- for (int i = 0; i < fields.size(); i++) {
- projects.add(RexInputRef.of2(projects.size(), fields));
- }
- final RelBuilder relBuilder = call.builder();
- relBuilder.push(aggregate)
- .projectNamed(Pair.left(projects), Pair.right(projects), true);
- aggOutputProject = (LogicalProject) relBuilder.build();
- }
- onMatch2(call, correlate, left, aggOutputProject, aggregate);
- }
-
- private void onMatch2(
- RelOptRuleCall call,
- LogicalCorrelate correlate,
- RelNode leftInput,
- LogicalProject aggOutputProject,
- LogicalAggregate aggregate) {
- if (generatedCorRels.contains(correlate)) {
- // This Correlate was generated by a previous invocation of
- // this rule. No further work to do.
- return;
- }
-
- setCurrent(call.getPlanner().getRoot(), correlate);
-
- // check for this pattern
- // The pattern matching could be simplified if rules can be applied
- // during decorrelation,
- //
- // CorrelateRel(left correlation, condition = true)
- // leftInput
- // Project-A (a RexNode)
- // Aggregate (groupby (0), agg0(), agg1()...)
-
- // check aggOutputProj projects only one expression
- List<RexNode> aggOutputProjExprs = aggOutputProject.getProjects();
- if (aggOutputProjExprs.size() != 1) {
- return;
- }
-
- JoinRelType joinType = correlate.getJoinType();
- // corRel.getCondition was here, however Correlate was updated so it
- // never includes a join condition. The code was not modified for brevity.
- RexNode joinCond = relBuilder.literal(true);
- if ((joinType != JoinRelType.LEFT)
- || (joinCond != relBuilder.literal(true))) {
- return;
- }
-
- // check that the agg is on the entire input
- if (!aggregate.getGroupSet().isEmpty()) {
- return;
- }
-
- List<AggregateCall> aggCalls = aggregate.getAggCallList();
- Set<Integer> isCount = new HashSet<>();
-
- // remember the count() positions
- int i = -1;
- for (AggregateCall aggCall : aggCalls) {
- ++i;
- if (aggCall.getAggregation() instanceof SqlCountAggFunction) {
- isCount.add(i);
- }
- }
-
- // now rewrite the plan to
- //
- // Project-A' (all LHS plus transformed original projections,
- // replacing references to count() with case statement)
- // Correlate(left correlation, condition = true)
- // leftInput
- // Aggregate(groupby (0), agg0(), agg1()...)
- //
- LogicalCorrelate newCorrelate =
- LogicalCorrelate.create(leftInput, aggregate,
- correlate.getCorrelationId(), correlate.getRequiredColumns(),
- correlate.getJoinType());
-
- // remember this rel so we don't fire rule on it again
- // REVIEW jhyde 29-Oct-2007: rules should not save state; rule
- // should recognize patterns where it does or does not need to do
- // work
- generatedCorRels.add(newCorrelate);
-
- // need to update the mapCorToCorRel Update the output position
- // for the corVars: only pass on the corVars that are not used in
- // the join key.
- if (cm.mapCorToCorRel.get(correlate.getCorrelationId()) == correlate) {
- cm.mapCorToCorRel.put(correlate.getCorrelationId(), newCorrelate);
- }
-
- RelNode newOutput =
- aggregateCorrelatorOutput(newCorrelate, aggOutputProject, isCount);
-
- call.transformTo(newOutput);
- }
- }
-
- /**
- * A unique reference to a correlation field.
- *
- * <p>For instance, if a RelNode references emp.name multiple times, it would
- * result in multiple {@code CorRef} objects that differ just in
- * {@link CorRef#uniqueKey}.
- */
- static class CorRef implements Comparable<CorRef> {
- public final int uniqueKey;
- public final CorrelationId corr;
- public final int field;
-
- CorRef(CorrelationId corr, int field, int uniqueKey) {
- this.corr = corr;
- this.field = field;
- this.uniqueKey = uniqueKey;
- }
-
- @Override public String toString() {
- return corr.getName() + '.' + field;
- }
-
- @Override public int hashCode() {
- return Objects.hash(uniqueKey, corr, field);
- }
-
- @Override public boolean equals(Object o) {
- return this == o
- || o instanceof CorRef
- && uniqueKey == ((CorRef) o).uniqueKey
- && corr == ((CorRef) o).corr
- && field == ((CorRef) o).field;
- }
-
- public int compareTo(@Nonnull CorRef o) {
- int c = corr.compareTo(o.corr);
- if (c != 0) {
- return c;
- }
- c = Integer.compare(field, o.field);
- if (c != 0) {
- return c;
- }
- return Integer.compare(uniqueKey, o.uniqueKey);
- }
-
- public CorDef def() {
- return new CorDef(corr, field);
- }
- }
-
- /** A correlation and a field. */
- static class CorDef implements Comparable<CorDef> {
- public final CorrelationId corr;
- public final int field;
-
- CorDef(CorrelationId corr, int field) {
- this.corr = corr;
- this.field = field;
- }
-
- @Override public String toString() {
- return corr.getName() + '.' + field;
- }
-
- @Override public int hashCode() {
- return Objects.hash(corr, field);
- }
-
- @Override public boolean equals(Object o) {
- return this == o
- || o instanceof CorDef
- && corr == ((CorDef) o).corr
- && field == ((CorDef) o).field;
- }
-
- public int compareTo(@Nonnull CorDef o) {
- int c = corr.compareTo(o.corr);
- if (c != 0) {
- return c;
- }
- return Integer.compare(field, o.field);
- }
- }
-
- /** A map of the locations of
- * {@link LogicalCorrelate}
- * in a tree of {@link RelNode}s.
- *
- * <p>It is used to drive the decorrelation process.
- * Treat it as immutable; rebuild if you modify the tree.
- *
- * <p>There are three maps:<ol>
- *
- * <li>{@link #mapRefRelToCorRef} maps a {@link RelNode} to the correlated
- * variables it references;
- *
- * <li>{@link #mapCorToCorRel} maps a correlated variable to the
- * {@link Correlate} providing it;
- *
- * <li>{@link #mapFieldAccessToCorRef} maps a rex field access to
- * the corVar it represents. Because typeFlattener does not clone or
- * modify a correlated field access this map does not need to be
- * updated.
- *
- * </ol> */
- private static class CorelMap {
- private final Multimap<RelNode, CorRef> mapRefRelToCorRef;
- private final SortedMap<CorrelationId, RelNode> mapCorToCorRel;
- private final Map<RexFieldAccess, CorRef> mapFieldAccessToCorRef;
-
- // TODO: create immutable copies of all maps
- private CorelMap(Multimap<RelNode, CorRef> mapRefRelToCorRef,
- SortedMap<CorrelationId, RelNode> mapCorToCorRel,
- Map<RexFieldAccess, CorRef> mapFieldAccessToCorRef) {
- this.mapRefRelToCorRef = mapRefRelToCorRef;
- this.mapCorToCorRel = mapCorToCorRel;
- this.mapFieldAccessToCorRef = ImmutableMap.copyOf(mapFieldAccessToCorRef);
- }
-
- @Override public String toString() {
- return "mapRefRelToCorRef=" + mapRefRelToCorRef
- + "\nmapCorToCorRel=" + mapCorToCorRel
- + "\nmapFieldAccessToCorRef=" + mapFieldAccessToCorRef
- + "\n";
- }
-
- @Override public boolean equals(Object obj) {
- return obj == this
- || obj instanceof CorelMap
- && mapRefRelToCorRef.equals(((CorelMap) obj).mapRefRelToCorRef)
- && mapCorToCorRel.equals(((CorelMap) obj).mapCorToCorRel)
- && mapFieldAccessToCorRef.equals(
- ((CorelMap) obj).mapFieldAccessToCorRef);
- }
-
- @Override public int hashCode() {
- return Objects.hash(mapRefRelToCorRef, mapCorToCorRel,
- mapFieldAccessToCorRef);
- }
-
- /** Creates a CorelMap with given contents. */
- public static CorelMap of(
- SortedSetMultimap<RelNode, CorRef> mapRefRelToCorVar,
- SortedMap<CorrelationId, RelNode> mapCorToCorRel,
- Map<RexFieldAccess, CorRef> mapFieldAccessToCorVar) {
- return new CorelMap(mapRefRelToCorVar, mapCorToCorRel,
- mapFieldAccessToCorVar);
- }
-
- /**
- * Returns whether there are any correlating variables in this statement.
- *
- * @return whether there are any correlating variables
- */
- public boolean hasCorrelation() {
- return !mapCorToCorRel.isEmpty();
- }
- }
-
- /** Builds a {@link CorelMap}. */
- private static class CorelMapBuilder extends RelShuttleImpl {
- final SortedMap<CorrelationId, RelNode> mapCorToCorRel =
- new TreeMap<>();
-
- final SortedSetMultimap<RelNode, CorRef> mapRefRelToCorRef =
- MultimapBuilder.SortedSetMultimapBuilder.hashKeys()
- .treeSetValues()
- .build();
-
- final Map<RexFieldAccess, CorRef> mapFieldAccessToCorVar = new HashMap<>();
-
- final Holder<Integer> offset = Holder.of(0);
- int corrIdGenerator = 0;
-
- /** Creates a CorelMap by iterating over a {@link RelNode} tree. */
- CorelMap build(RelNode... rels) {
- for (RelNode rel : rels) {
- stripHep(rel).accept(this);
- }
- return new CorelMap(mapRefRelToCorRef, mapCorToCorRel,
- mapFieldAccessToCorVar);
- }
-
- @Override public RelNode visit(LogicalJoin join) {
- try {
- stack.push(join);
- join.getCondition().accept(rexVisitor(join));
- } finally {
- stack.pop();
- }
- return visitJoin(join);
- }
-
- @Override protected RelNode visitChild(RelNode parent, int i,
- RelNode input) {
- return super.visitChild(parent, i, stripHep(input));
- }
-
- @Override public RelNode visit(LogicalCorrelate correlate) {
- mapCorToCorRel.put(correlate.getCorrelationId(), correlate);
- return visitJoin(correlate);
- }
-
- private RelNode visitJoin(BiRel join) {
- final int x = offset.get();
- visitChild(join, 0, join.getLeft());
- offset.set(x + join.getLeft().getRowType().getFieldCount());
- visitChild(join, 1, join.getRight());
- offset.set(x);
- return join;
- }
-
- @Override public RelNode visit(final LogicalFilter filter) {
- try {
- stack.push(filter);
- filter.getCondition().accept(rexVisitor(filter));
- } finally {
- stack.pop();
- }
- return super.visit(filter);
- }
-
- @Override public RelNode visit(LogicalProject project) {
- try {
- stack.push(project);
- for (RexNode node : project.getProjects()) {
- node.accept(rexVisitor(project));
- }
- } finally {
- stack.pop();
- }
- return super.visit(project);
- }
-
- private RexVisitorImpl<Void> rexVisitor(final RelNode rel) {
- return new RexVisitorImpl<Void>(true) {
- @Override public Void visitFieldAccess(RexFieldAccess fieldAccess) {
- final RexNode ref = fieldAccess.getReferenceExpr();
- if (ref instanceof RexCorrelVariable) {
- final RexCorrelVariable var = (RexCorrelVariable) ref;
- if (mapFieldAccessToCorVar.containsKey(fieldAccess)) {
- // for cases where different Rel nodes are referring to
- // same correlation var (e.g. in case of NOT IN)
- // avoid generating another correlation var
- // and record the 'rel' is using the same correlation
- mapRefRelToCorRef.put(rel,
- mapFieldAccessToCorVar.get(fieldAccess));
- } else {
- final CorRef correlation =
- new CorRef(var.id, fieldAccess.getField().getIndex(),
- corrIdGenerator++);
- mapFieldAccessToCorVar.put(fieldAccess, correlation);
- mapRefRelToCorRef.put(rel, correlation);
- }
- }
- return super.visitFieldAccess(fieldAccess);
- }
-
- @Override public Void visitSubQuery(RexSubQuery subQuery) {
- subQuery.rel.accept(CorelMapBuilder.this);
- return super.visitSubQuery(subQuery);
- }
- };
- }
- }
-
- /** Frame describing the relational expression after decorrelation
- * and where to find the output fields and correlation variables
- * among its output fields. */
- static class Frame {
- final RelNode r;
- final ImmutableSortedMap<CorDef, Integer> corDefOutputs;
- final ImmutableSortedMap<Integer, Integer> oldToNewOutputs;
-
- Frame(RelNode oldRel, RelNode r, SortedMap<CorDef, Integer> corDefOutputs,
- Map<Integer, Integer> oldToNewOutputs) {
- this.r = Objects.requireNonNull(r);
- this.corDefOutputs = ImmutableSortedMap.copyOf(corDefOutputs);
- this.oldToNewOutputs = ImmutableSortedMap.copyOf(oldToNewOutputs);
- assert allLessThan(this.corDefOutputs.values(),
- r.getRowType().getFieldCount(), Litmus.THROW);
- assert allLessThan(this.oldToNewOutputs.keySet(),
- oldRel.getRowType().getFieldCount(), Litmus.THROW);
- assert allLessThan(this.oldToNewOutputs.values(),
- r.getRowType().getFieldCount(), Litmus.THROW);
- }
- }
-}
-
-// End RelDecorrelator.java