throw newValidationError()

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)) {