in core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java [6255:6879]
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()));
}
}
PairList<String, RelDataType> measureColumns =
validateMeasure(matchRecognize, scope, allRows);
measureColumns.forEach((name, type) -> {
if (!typeBuilder.nameExists(name)) {
typeBuilder.add(name, type);
}
});
final RelDataType rowType;
if (matchRecognize.getMeasureList().isEmpty()) {
rowType = getNamespaceOrThrow(matchRecognize.getTableRef()).getRowType();
} else {
rowType = typeBuilder.build();
}
ns.setType(rowType);
}
private PairList<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 PairList<String, RelDataType> fields = PairList.of();
for (SqlNode measure : measures) {
assert measure instanceof SqlCall;
final String alias = SqlValidatorUtil.alias(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(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()) {
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()) {
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();
}
public void validatePivot(SqlPivot pivot) {
final PivotScope scope = (PivotScope) getJoinScope(pivot);
final PivotNamespace ns =
getNamespaceOrThrow(pivot).unwrap(PivotNamespace.class);
assert ns.rowType == null;
// Given
// query PIVOT (agg1 AS a, agg2 AS b, ...
// FOR (axis1, ..., axisN)
// IN ((v11, ..., v1N) AS label1,
// (v21, ..., v2N) AS label2, ...))
// the type is
// k1, ... kN, a_label1, b_label1, ..., a_label2, b_label2, ...
// where k1, ... kN are columns that are not referenced as an argument to
// an aggregate or as an axis.
// Aggregates, e.g. "PIVOT (sum(x) AS sum_x, count(*) AS c)"
final PairList<@Nullable String, RelDataType> aggNames = PairList.of();
pivot.forEachAgg((alias, call) -> {
call.validate(this, scope);
final RelDataType type = deriveType(scope, call);
aggNames.add(alias, type);
if (!(call instanceof SqlCall)
|| !(((SqlCall) call).getOperator() instanceof SqlAggFunction)) {
throw newValidationError(call, RESOURCE.pivotAggMalformed());
}
});
// Axes, e.g. "FOR (JOB, DEPTNO)"
final List<RelDataType> axisTypes = new ArrayList<>();
final List<SqlIdentifier> axisIdentifiers = new ArrayList<>();
for (SqlNode axis : pivot.axisList) {
SqlIdentifier identifier = (SqlIdentifier) axis;
identifier.validate(this, scope);
final RelDataType type = deriveType(scope, identifier);
axisTypes.add(type);
axisIdentifiers.add(identifier);
}
// Columns that have been seen as arguments to aggregates or as axes
// do not appear in the output.
final Set<String> columnNames = pivot.usedColumnNames();
final RelDataTypeFactory.Builder typeBuilder = typeFactory.builder();
scope.getChild().getRowType().getFieldList().forEach(field -> {
if (!columnNames.contains(field.getName())) {
typeBuilder.add(field);
}
});
// Values, e.g. "IN (('CLERK', 10) AS c10, ('MANAGER, 20) AS m20)"
pivot.forEachNameValues((alias, nodeList) -> {
if (nodeList.size() != axisTypes.size()) {
throw newValidationError(nodeList,
RESOURCE.pivotValueArityMismatch(nodeList.size(),
axisTypes.size()));
}
final SqlOperandTypeChecker typeChecker =
OperandTypes.COMPARABLE_UNORDERED_COMPARABLE_UNORDERED;
Pair.forEach(axisIdentifiers, nodeList, (identifier, subNode) -> {
subNode.validate(this, scope);
typeChecker.checkOperandTypes(
new SqlCallBinding(this, scope,
SqlStdOperatorTable.EQUALS.createCall(
subNode.getParserPosition(), identifier, subNode)),
true);
});
aggNames.forEach((aggAlias, aggType) ->
typeBuilder.add(aggAlias == null ? alias : alias + "_" + aggAlias,
aggType));
});
final RelDataType rowType = typeBuilder.build();
ns.setType(rowType);
}
public void validateUnpivot(SqlUnpivot unpivot) {
final UnpivotScope scope = (UnpivotScope) getJoinScope(unpivot);
final UnpivotNamespace ns =
getNamespaceOrThrow(unpivot).unwrap(UnpivotNamespace.class);
assert ns.rowType == null;
// Given
// query UNPIVOT ((measure1, ..., measureM)
// FOR (axis1, ..., axisN)
// IN ((c11, ..., c1M) AS (value11, ..., value1N),
// (c21, ..., c2M) AS (value21, ..., value2N), ...)
// the type is
// k1, ... kN, axis1, ..., axisN, measure1, ..., measureM
// where k1, ... kN are columns that are not referenced as an argument to
// an aggregate or as an axis.
// First, And make sure that each
final int measureCount = unpivot.measureList.size();
final int axisCount = unpivot.axisList.size();
unpivot.forEachNameValues((nodeList, valueList) -> {
// Make sure that each (ci1, ... ciM) list has the same arity as
// (measure1, ..., measureM).
if (nodeList.size() != measureCount) {
throw newValidationError(nodeList,
RESOURCE.unpivotValueArityMismatch(nodeList.size(),
measureCount));
}
// Make sure that each (vi1, ... viN) list has the same arity as
// (axis1, ..., axisN).
if (valueList != null && valueList.size() != axisCount) {
throw newValidationError(valueList,
RESOURCE.unpivotValueArityMismatch(valueList.size(),
axisCount));
}
// Make sure that each IN expression is a valid column from the input.
nodeList.forEach(node -> deriveType(scope, node));
});
// What columns from the input are not referenced by a column in the IN
// list?
final SqlValidatorNamespace inputNs =
requireNonNull(getNamespace(unpivot.query));
final Set<String> unusedColumnNames =
catalogReader.nameMatcher().createSet();
unusedColumnNames.addAll(inputNs.getRowType().getFieldNames());
unusedColumnNames.removeAll(unpivot.usedColumnNames());
// What columns will be present in the output row type?
final Set<String> columnNames = catalogReader.nameMatcher().createSet();
columnNames.addAll(unusedColumnNames);
// Gather the name and type of each measure.
final PairList<String, RelDataType> measureNameTypes = PairList.of();
forEach(unpivot.measureList, (measure, i) -> {
final String measureName = ((SqlIdentifier) measure).getSimple();
final List<RelDataType> types = new ArrayList<>();
final List<SqlNode> nodes = new ArrayList<>();
unpivot.forEachNameValues((nodeList, valueList) -> {
final SqlNode alias = nodeList.get(i);
nodes.add(alias);
types.add(deriveType(scope, alias));
});
final RelDataType type0 = typeFactory.leastRestrictive(types);
if (type0 == null) {
throw newValidationError(nodes.get(0),
RESOURCE.unpivotCannotDeriveMeasureType(measureName));
}
final RelDataType type =
typeFactory.createTypeWithNullability(type0,
unpivot.includeNulls || unpivot.measureList.size() > 1);
setValidatedNodeType(measure, type);
if (!columnNames.add(measureName)) {
throw newValidationError(measure,
RESOURCE.unpivotDuplicate(measureName));
}
measureNameTypes.add(measureName, type);
});
// Gather the name and type of each axis.
// Consider
// FOR (job, deptno)
// IN (a AS ('CLERK', 10),
// b AS ('ANALYST', 20))
// There are two axes, (job, deptno), and so each value list ('CLERK', 10),
// ('ANALYST', 20) must have arity two.
//
// The type of 'job' is derived as the least restrictive type of the values
// ('CLERK', 'ANALYST'), namely VARCHAR(7). The derived type of 'deptno' is
// the type of values (10, 20), namely INTEGER.
final PairList<String, RelDataType> axisNameTypes = PairList.of();
forEach(unpivot.axisList, (axis, i) -> {
final String axisName = ((SqlIdentifier) axis).getSimple();
final List<RelDataType> types = new ArrayList<>();
unpivot.forEachNameValues((aliasList, valueList) ->
types.add(
valueList == null
? typeFactory.createSqlType(SqlTypeName.VARCHAR,
SqlUnpivot.aliasValue(aliasList).length())
: deriveType(scope, valueList.get(i))));
final RelDataType type = typeFactory.leastRestrictive(types);
if (type == null) {
throw newValidationError(axis,
RESOURCE.unpivotCannotDeriveAxisType(axisName));
}
setValidatedNodeType(axis, type);
if (!columnNames.add(axisName)) {
throw newValidationError(axis, RESOURCE.unpivotDuplicate(axisName));
}
axisNameTypes.add(axisName, type);
});
// Columns that have been seen as arguments to aggregates or as axes
// do not appear in the output.
final RelDataTypeFactory.Builder typeBuilder = typeFactory.builder();
scope.getChild().getRowType().getFieldList().forEach(field -> {
if (unusedColumnNames.contains(field.getName())) {
typeBuilder.add(field);
}
});
typeBuilder.addAll(axisNameTypes);
typeBuilder.addAll(measureNameTypes);
final RelDataType rowType = typeBuilder.build();
ns.setType(rowType);
}
/** 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;
}
@Override public void validateAggregateParams(SqlCall aggCall,
@Nullable SqlNode filter, @Nullable SqlNodeList distinctList,
@Nullable 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 (distinctList != null) {
for (SqlNode param : distinctList) {
if (a.findAgg(param) != null) {
throw newValidationError(aggCall,
RESOURCE.aggregateInWithinDistinctIllegal());
}
}
}
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.isEmpty()) {
throw newValidationError(aggCall,
RESOURCE.aggregateMissingWithinGroupClause(op.getName()));
}
break;
case OPTIONAL:
break;
case IGNORED:
// rewrite the order list to empty
if (orderList != null) {
orderList.clear();
}
break;
case FORBIDDEN:
if (orderList != null && !orderList.isEmpty()) {
throw newValidationError(aggCall,
RESOURCE.withinGroupClauseIllegalInAggregate(op.getName()));
}
break;
default:
throw new AssertionError(op);
}
// Because there are two forms of the PERCENTILE_CONT/PERCENTILE_DISC functions,
// they are distinguished by their operand count and then validated accordingly.
// For example, the standard single operand form requires group order while the
// 2-operand form allows for null treatment and requires an OVER() clause.
if (op.isPercentile()) {
switch (aggCall.operandCount()) {
case 1:
assert op.requiresGroupOrder() == Optionality.MANDATORY;
assert orderList != null;
// Validate that percentile function have a single ORDER BY expression
if (orderList.size() != 1) {
throw newValidationError(orderList,
RESOURCE.orderByRequiresOneKey(op.getName()));
}
// Validate that the ORDER BY field is of NUMERIC type
SqlNode node = requireNonNull(orderList.get(0));
final RelDataType type = deriveType(scope, node);
final @Nullable SqlTypeFamily family = type.getSqlTypeName().getFamily();
if (family == null
|| family.allowableDifferenceTypes().isEmpty()) {
throw newValidationError(orderList,
RESOURCE.unsupportedTypeInOrderBy(
type.getSqlTypeName().getName(),
op.getName()));
}
break;
case 2:
assert op.allowsNullTreatment();
assert op.requiresOver();
assert op.requiresGroupOrder() == Optionality.FORBIDDEN;
break;
default:
throw newValidationError(aggCall, RESOURCE.percentileFunctionsArgumentLimit());
}
}
}
@Override public void validateCall(
SqlCall call,
SqlValidatorScope scope) {
final SqlOperator operator = call.getOperator();
if ((call.operandCount() == 0)
&& (operator.getSyntax() == SqlSyntax.FUNCTION_ID)
&& !call.isExpanded()
&& !this.config.conformance().allowNiladicParentheses()) {
// For example, "LOCALTIME()" is illegal. (It should be
// "LOCALTIME", which would have been handled as a
// SqlIdentifier.)
throw handleUnresolvedFunction(call, 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;
}
@Override public SqlLiteral resolveLiteral(SqlLiteral literal) {
switch (literal.getTypeName()) {
case UNKNOWN:
final SqlUnknownLiteral unknownLiteral = (SqlUnknownLiteral) literal;
final SqlIdentifier identifier =
new SqlIdentifier(unknownLiteral.tag, SqlParserPos.ZERO);
final @Nullable RelDataType type = catalogReader.getNamedType(identifier);
final SqlTypeName typeName;
if (type != null) {
typeName = type.getSqlTypeName();
} else {
typeName = SqlTypeName.lookup(unknownLiteral.tag);
}
return unknownLiteral.resolve(typeName);
default:
return literal;
}
}
public SqlNode expandSelectExpr(SqlNode expr,
SelectScope scope, SqlSelect select, Map<String, SqlNode> expansions) {
final Expander expander = new SelectExpander(this, scope, select, expansions);
final SqlNode newExpr = expander.go(expr);
if (expr != newExpr) {
setOriginal(newExpr, expr);
}
return newExpr;
}
@Override public SqlNode expand(SqlNode expr, SqlValidatorScope scope) {
final Expander expander = new Expander(this, scope);
SqlNode newExpr = expander.go(expr);
if (expr != newExpr) {
setOriginal(newExpr, expr);
}
return newExpr;
}
/** Expands an expression in a GROUP BY, HAVING or QUALIFY clause. */
private SqlNode extendedExpand(SqlNode expr,
SqlValidatorScope scope, SqlSelect select, Clause clause) {
final Expander expander =
new ExtendedExpander(this, scope, select, expr, clause);
SqlNode newExpr = expander.go(expr);
if (expr != newExpr) {
setOriginal(newExpr, expr);
}
return newExpr;
}
public SqlNode extendedExpandGroupBy(SqlNode expr,
SqlValidatorScope scope, SqlSelect select) {
return extendedExpand(expr, scope, select, Clause.GROUP_BY);
}
@Override public boolean isSystemField(RelDataTypeField field) {
return false;
}
@Override public List<@Nullable List<String>> getFieldOrigins(SqlNode sqlQuery) {
if (sqlQuery instanceof SqlExplain) {
return emptyList();
}
final RelDataType rowType = getValidatedNodeType(sqlQuery);
final int fieldCount = rowType.getFieldCount();
if (!sqlQuery.isA(SqlKind.QUERY)) {
return Collections.nCopies(fieldCount, null);
}
final List<@Nullable List<String>> list = new ArrayList<>();
for (int i = 0; i < fieldCount; i++) {
list.add(getFieldOrigin(sqlQuery, i));
}
return ImmutableNullableList.copyOf(list);
}
private @Nullable List<String> getFieldOrigin(SqlNode sqlQuery, int i) {
if (sqlQuery instanceof SqlSelect) {
SqlSelect sqlSelect = (SqlSelect) sqlQuery;
final SelectScope scope = getRawSelectScopeNonNull(sqlSelect);
final List<SqlNode> selectList =
requireNonNull(scope.getExpandedSelectList(),
() -> "expandedSelectList for " + scope);
final SqlNode selectItem = stripAs(selectList.get(i));
if (selectItem instanceof SqlIdentifier) {
final SqlQualified qualified =
scope.fullyQualify((SqlIdentifier) selectItem);
SqlValidatorNamespace namespace =
requireNonNull(qualified.namespace,
() -> "namespace for " + qualified);
if (namespace.isWrapperFor(AliasNamespace.class)) {
AliasNamespace aliasNs = namespace.unwrap(AliasNamespace.class);
SqlNode aliased = requireNonNull(aliasNs.getNode(), () ->
"sqlNode for aliasNs " + aliasNs);
namespace = getNamespaceOrThrow(stripAs(aliased));
}
final SqlValidatorTable table = namespace.getTable();
if (table == null) {
return null;
}
final List<String> origin =
new ArrayList<>(table.getQualifiedName());
for (String name : qualified.suffix()) {
if (namespace.isWrapperFor(UnnestNamespace.class)) {