| /* |
| * 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.config.CalciteConnectionConfig; |
| import org.apache.calcite.config.CalciteConnectionConfigImpl; |
| import org.apache.calcite.config.CalciteConnectionProperty; |
| import org.apache.calcite.jdbc.CalciteSchema; |
| import org.apache.calcite.linq4j.Linq4j; |
| import org.apache.calcite.linq4j.Ord; |
| import org.apache.calcite.plan.RelOptSchemaWithSampling; |
| import org.apache.calcite.plan.RelOptTable; |
| import org.apache.calcite.prepare.CalciteCatalogReader; |
| import org.apache.calcite.prepare.Prepare; |
| import org.apache.calcite.rel.core.JoinRelType; |
| 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.RelDataTypeFieldImpl; |
| import org.apache.calcite.runtime.PairList; |
| import org.apache.calcite.schema.CustomColumnResolvingTable; |
| import org.apache.calcite.schema.ExtensibleTable; |
| import org.apache.calcite.schema.Table; |
| import org.apache.calcite.schema.impl.AbstractSchema; |
| import org.apache.calcite.schema.impl.AbstractTable; |
| import org.apache.calcite.sql.SqlCall; |
| import org.apache.calcite.sql.SqlCallBinding; |
| import org.apache.calcite.sql.SqlDataTypeSpec; |
| import org.apache.calcite.sql.SqlDynamicParam; |
| import org.apache.calcite.sql.SqlFunctionCategory; |
| import org.apache.calcite.sql.SqlIdentifier; |
| import org.apache.calcite.sql.SqlIntervalQualifier; |
| import org.apache.calcite.sql.SqlKind; |
| import org.apache.calcite.sql.SqlLiteral; |
| import org.apache.calcite.sql.SqlNode; |
| import org.apache.calcite.sql.SqlNodeList; |
| import org.apache.calcite.sql.SqlOperator; |
| import org.apache.calcite.sql.SqlOperatorBinding; |
| import org.apache.calcite.sql.SqlOperatorTable; |
| import org.apache.calcite.sql.SqlSelect; |
| import org.apache.calcite.sql.SqlSyntax; |
| import org.apache.calcite.sql.SqlUtil; |
| import org.apache.calcite.sql.fun.SqlStdOperatorTable; |
| import org.apache.calcite.sql.parser.SqlParserPos; |
| import org.apache.calcite.sql.type.SqlTypeUtil; |
| import org.apache.calcite.util.ImmutableBitSet; |
| import org.apache.calcite.util.Litmus; |
| import org.apache.calcite.util.Pair; |
| import org.apache.calcite.util.Util; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import static org.apache.calcite.linq4j.Nullness.castNonNullList; |
| import static org.apache.calcite.sql.type.NonNullableAccessors.getCharset; |
| import static org.apache.calcite.sql.type.NonNullableAccessors.getCollation; |
| import static org.apache.calcite.util.Static.RESOURCE; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| /** |
| * Utility methods related to validation. |
| */ |
| public class SqlValidatorUtil { |
| private SqlValidatorUtil() {} |
| |
| //~ Methods ---------------------------------------------------------------- |
| |
| /** |
| * Converts a {@link SqlValidatorScope} into a {@link RelOptTable}. This is |
| * only possible if the scope represents an identifier, such as "sales.emp". |
| * Otherwise, returns null. |
| * |
| * @param namespace Namespace |
| * @param catalogReader Schema |
| * @param datasetName Name of sample dataset to substitute, or null to use |
| * the regular table |
| * @param usedDataset Output parameter which is set to true if a sample |
| * dataset is found; may be null |
| */ |
| public static @Nullable RelOptTable getRelOptTable( |
| SqlValidatorNamespace namespace, |
| Prepare.@Nullable CatalogReader catalogReader, |
| @Nullable String datasetName, |
| boolean @Nullable [] usedDataset) { |
| if (namespace.isWrapperFor(TableNamespace.class)) { |
| final TableNamespace tableNamespace = |
| namespace.unwrap(TableNamespace.class); |
| return getRelOptTable(tableNamespace, |
| requireNonNull(catalogReader, "catalogReader"), datasetName, usedDataset, |
| tableNamespace.extendedFields); |
| } else if (namespace.isWrapperFor(SqlValidatorImpl.DmlNamespace.class)) { |
| final SqlValidatorImpl.DmlNamespace dmlNamespace = |
| namespace.unwrap(SqlValidatorImpl.DmlNamespace.class); |
| final SqlValidatorNamespace resolvedNamespace = dmlNamespace.resolve(); |
| if (resolvedNamespace.isWrapperFor(TableNamespace.class)) { |
| final TableNamespace tableNamespace = resolvedNamespace.unwrap(TableNamespace.class); |
| final SqlValidatorTable validatorTable = tableNamespace.getTable(); |
| final List<RelDataTypeField> extendedFields = dmlNamespace.extendList == null |
| ? ImmutableList.of() |
| : getExtendedColumns(namespace.getValidator(), validatorTable, dmlNamespace.extendList); |
| return getRelOptTable( |
| tableNamespace, requireNonNull(catalogReader, "catalogReader"), |
| datasetName, usedDataset, extendedFields); |
| } |
| } |
| return null; |
| } |
| |
| private static @Nullable RelOptTable getRelOptTable( |
| TableNamespace tableNamespace, |
| Prepare.CatalogReader catalogReader, |
| @Nullable String datasetName, |
| boolean @Nullable [] usedDataset, |
| List<RelDataTypeField> extendedFields) { |
| final List<String> names = tableNamespace.getTable().getQualifiedName(); |
| RelOptTable table; |
| if (datasetName != null |
| && catalogReader instanceof RelOptSchemaWithSampling) { |
| final RelOptSchemaWithSampling reader = |
| (RelOptSchemaWithSampling) catalogReader; |
| table = reader.getTableForMember(names, datasetName, usedDataset); |
| } else { |
| // Schema does not support substitution. Ignore the data set, if any. |
| table = catalogReader.getTableForMember(names); |
| } |
| if (table != null && !extendedFields.isEmpty()) { |
| table = table.extend(extendedFields); |
| } |
| return table; |
| } |
| |
| /** |
| * Gets a list of extended columns with field indices to the underlying table. |
| */ |
| public static List<RelDataTypeField> getExtendedColumns( |
| SqlValidator validator, SqlValidatorTable table, |
| SqlNodeList extendedColumns) { |
| final ImmutableList.Builder<RelDataTypeField> extendedFields = |
| ImmutableList.builder(); |
| final ExtensibleTable extTable = table.unwrap(ExtensibleTable.class); |
| int extendedFieldOffset = |
| extTable == null |
| ? table.getRowType().getFieldCount() |
| : extTable.getExtendedColumnOffset(); |
| pairs(extendedColumns).forEachIndexed((i, identifier, type) -> |
| extendedFields.add( |
| new RelDataTypeFieldImpl(identifier.toString(), |
| extendedFieldOffset + i, |
| type.deriveType(requireNonNull(validator, "validator"))))); |
| return extendedFields.build(); |
| } |
| |
| /** Converts a list of extended columns |
| * (of the form [name0, type0, name1, type1, ...]) |
| * into a list of (name, type) pairs. */ |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private static PairList<SqlIdentifier, SqlDataTypeSpec> pairs( |
| SqlNodeList extendedColumns) { |
| return PairList.backedBy((List) extendedColumns.getList()); |
| } |
| |
| /** |
| * Gets a map of indexes from the source to fields in the target for the |
| * intersecting set of source and target fields. |
| * |
| * @param sourceFields The source of column names that determine indexes |
| * @param targetFields The target fields to be indexed |
| */ |
| public static ImmutableMap<Integer, RelDataTypeField> getIndexToFieldMap( |
| List<RelDataTypeField> sourceFields, |
| RelDataType targetFields) { |
| final ImmutableMap.Builder<Integer, RelDataTypeField> output = |
| ImmutableMap.builder(); |
| for (final RelDataTypeField source : sourceFields) { |
| final RelDataTypeField target = targetFields.getField(source.getName(), true, false); |
| if (target != null) { |
| output.put(source.getIndex(), target); |
| } |
| } |
| return output.build(); |
| } |
| |
| /** |
| * Gets the bit-set to the column ordinals in the source for columns that |
| * intersect in the target. |
| * |
| * @param sourceRowType The source upon which to ordinate the bit set. |
| * @param targetRowType The target to overlay on the source to create the bit set. |
| */ |
| public static ImmutableBitSet getOrdinalBitSet( |
| RelDataType sourceRowType, RelDataType targetRowType) { |
| Map<Integer, RelDataTypeField> indexToField = |
| getIndexToFieldMap(sourceRowType.getFieldList(), targetRowType); |
| return getOrdinalBitSet(sourceRowType, indexToField); |
| } |
| |
| /** |
| * Gets the bit-set to the column ordinals in the source for columns that |
| * intersect in the target. |
| * |
| * @param sourceRowType The source upon which to ordinate the bit set. |
| * @param indexToField The map of ordinals to target fields. |
| */ |
| public static ImmutableBitSet getOrdinalBitSet( |
| RelDataType sourceRowType, |
| Map<Integer, RelDataTypeField> indexToField) { |
| ImmutableBitSet source = |
| ImmutableBitSet.of( |
| Util.transform(sourceRowType.getFieldList(), |
| RelDataTypeField::getIndex)); |
| // checkerframework: found : Set<@KeyFor("indexToField") Integer> |
| //noinspection RedundantCast |
| ImmutableBitSet target = |
| ImmutableBitSet.of((Iterable<Integer>) indexToField.keySet()); |
| return source.intersect(target); |
| } |
| |
| /** Returns a map from field names to indexes. */ |
| public static Map<String, Integer> mapNameToIndex(List<RelDataTypeField> fields) { |
| ImmutableMap.Builder<String, Integer> output = ImmutableMap.builder(); |
| for (RelDataTypeField field : fields) { |
| output.put(field.getName(), field.getIndex()); |
| } |
| return output.build(); |
| } |
| |
| @Deprecated // to be removed before 2.0 |
| public static @Nullable RelDataTypeField lookupField(boolean caseSensitive, |
| final RelDataType rowType, String columnName) { |
| return rowType.getField(columnName, caseSensitive, false); |
| } |
| |
| public static void checkCharsetAndCollateConsistentIfCharType( |
| RelDataType type) { |
| // (every charset must have a default collation) |
| if (SqlTypeUtil.inCharFamily(type)) { |
| Charset strCharset = getCharset(type); |
| Charset colCharset = getCollation(type).getCharset(); |
| if (!strCharset.equals(colCharset)) { |
| if (false) { |
| // todo: enable this checking when we have a charset to |
| // collation mapping |
| throw new Error(type.toString() |
| + " was found to have charset '" + strCharset.name() |
| + "' and a mismatched collation charset '" |
| + colCharset.name() + "'"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks that there are no duplicates in a list of {@link SqlIdentifier}. |
| */ |
| static void checkIdentifierListForDuplicates(List<? extends @Nullable SqlNode> columnList, |
| SqlValidatorImpl.ValidationErrorFunction validationErrorFunction) { |
| final List<List<String>> names = |
| Util.transform(columnList, |
| node -> ((SqlIdentifier) requireNonNull(node, "node")).names); |
| final int i = Util.firstDuplicate(names); |
| if (i >= 0) { |
| throw validationErrorFunction.apply( |
| requireNonNull(columnList.get(i), () -> columnList + ".get(" + i + ")"), |
| RESOURCE.duplicateNameInColumnList(Util.last(names.get(i)))); |
| } |
| } |
| |
| /** |
| * Converts an expression "expr" into "expr AS alias". |
| */ |
| public static SqlNode addAlias( |
| SqlNode expr, |
| String alias) { |
| final SqlParserPos pos = expr.getParserPosition(); |
| final SqlIdentifier id = new SqlIdentifier(alias, pos); |
| return SqlStdOperatorTable.AS.createCall(pos, expr, id); |
| } |
| |
| /** |
| * Derives an alias for a node, and invents a mangled identifier if it |
| * cannot. |
| * |
| * <p>Examples: |
| * |
| * <ul> |
| * <li>Alias: "1 + 2 as foo" yields "foo" |
| * <li>Identifier: "foo.bar.baz" yields "baz" |
| * <li>Anything else yields "expr$<i>ordinal</i>" |
| * </ul> |
| * |
| * @param node Node |
| * @param ordinal Ordinal in SELECT clause (must be ≥ 0) |
| * |
| * @return An alias, if one can be derived; or a synthetic alias |
| * "expr$<i>ordinal</i>"; never null |
| */ |
| public static String alias(SqlNode node, int ordinal) { |
| checkArgument(ordinal >= 0); |
| return requireNonNull(alias_(node, ordinal), "alias"); |
| } |
| |
| public static @Nullable String alias(SqlNode node) { |
| return alias_(node, -1); |
| } |
| |
| /** Derives an alias for a node, and invents a mangled identifier if it |
| * cannot. |
| * |
| * @deprecated Use {@link #alias(SqlNode)} if {@code ordinal} is negative, |
| * or {@link #alias(SqlNode, int)} if {@code ordinal} is non-negative. */ |
| @Deprecated // to be removed before 2.0 |
| public static @Nullable String getAlias(SqlNode node, int ordinal) { |
| return alias_(node, ordinal); |
| } |
| |
| /** Returns an alias, if one can be derived; or a synthetic alias |
| * "expr$<i>ordinal</i>" if ordinal ≥ 0; otherwise null. */ |
| private static @Nullable String alias_(SqlNode node, int ordinal) { |
| switch (node.getKind()) { |
| case AS: |
| // E.g. "1 + 2 as foo" --> "foo" |
| return ((SqlCall) node).operand(1).toString(); |
| |
| case OVER: |
| // E.g. "bids over w" --> "bids" |
| return getAlias(((SqlCall) node).operand(0), ordinal); |
| |
| case IDENTIFIER: |
| // E.g. "foo.bar" --> "bar" |
| return Util.last(((SqlIdentifier) node).names); |
| |
| default: |
| if (ordinal < 0) { |
| return null; |
| } else { |
| return SqlUtil.deriveAliasFromOrdinal(ordinal); |
| } |
| } |
| } |
| |
| /** |
| * Factory method for {@link SqlValidator}. |
| */ |
| public static SqlValidatorWithHints newValidator( |
| SqlOperatorTable opTab, |
| SqlValidatorCatalogReader catalogReader, |
| RelDataTypeFactory typeFactory, |
| SqlValidator.Config config) { |
| return new SqlValidatorImpl(opTab, catalogReader, typeFactory, |
| config); |
| } |
| |
| /** |
| * Factory method for {@link SqlValidator}, with default conformance. |
| */ |
| @Deprecated // to be removed before 2.0 |
| public static SqlValidatorWithHints newValidator( |
| SqlOperatorTable opTab, |
| SqlValidatorCatalogReader catalogReader, |
| RelDataTypeFactory typeFactory) { |
| return newValidator(opTab, catalogReader, typeFactory, |
| SqlValidator.Config.DEFAULT); |
| } |
| |
| /** |
| * Makes a name distinct from other names which have already been used, adds |
| * it to the list, and returns it. |
| * |
| * @param name Suggested name, may not be unique |
| * @param usedNames Collection of names already used |
| * @param suggester Base for name when input name is null |
| * @return Unique name |
| */ |
| public static String uniquify(@Nullable String name, Set<String> usedNames, |
| Suggester suggester) { |
| if (name != null) { |
| if (usedNames.add(name)) { |
| return name; |
| } |
| } |
| final String originalName = name; |
| for (int j = 0;; j++) { |
| name = suggester.apply(originalName, j, usedNames.size()); |
| if (usedNames.add(name)) { |
| return name; |
| } |
| } |
| } |
| |
| /** |
| * Makes sure that the names in a list are unique. |
| * |
| * <p>Does not modify the input list. Returns the input list if the strings |
| * are unique, otherwise allocates a new list. Deprecated in favor of caseSensitive |
| * aware version. |
| * |
| * @param nameList List of strings |
| * @return List of unique strings |
| */ |
| @Deprecated // to be removed before 2.0 |
| public static List<String> uniquify(List<? extends @Nullable String> nameList) { |
| return uniquify(nameList, EXPR_SUGGESTER, true); |
| } |
| |
| |
| /** |
| * Makes sure that the names in a list are unique. |
| * |
| * <p>Does not modify the input list. Returns the input list if the strings |
| * are unique, otherwise allocates a new list. |
| * |
| * @deprecated Use {@link #uniquify(List, Suggester, boolean)} |
| * |
| * @param nameList List of strings |
| * @param suggester How to generate new names if duplicate names are found |
| * @return List of unique strings |
| */ |
| @Deprecated // to be removed before 2.0 |
| public static List<String> uniquify(List<? extends @Nullable String> nameList, |
| Suggester suggester) { |
| return uniquify(nameList, suggester, true); |
| } |
| |
| /** |
| * Makes sure that the names in a list are unique. |
| * |
| * <p>Does not modify the input list. Returns the input list if the strings |
| * are unique, otherwise allocates a new list. |
| * |
| * @param nameList List of strings |
| * @param caseSensitive Whether upper and lower case names are considered |
| * distinct |
| * @return List of unique strings |
| */ |
| public static List<String> uniquify(List<? extends @Nullable String> nameList, |
| boolean caseSensitive) { |
| return uniquify(nameList, EXPR_SUGGESTER, caseSensitive); |
| } |
| |
| /** |
| * Makes sure that the names in a list are unique. |
| * |
| * <p>Does not modify the input list. Returns the input list if the strings |
| * are unique, otherwise allocates a new list. |
| * |
| * @param nameList List of strings |
| * @param suggester How to generate new names if duplicate names are found |
| * @param caseSensitive Whether upper and lower case names are considered |
| * distinct |
| * @return List of unique strings |
| */ |
| public static List<String> uniquify( |
| List<? extends @Nullable String> nameList, |
| Suggester suggester, |
| boolean caseSensitive) { |
| final Set<String> used; |
| if (caseSensitive) { |
| // Shortcut (avoiding creating a hash map) if the list is short and has |
| // no nulls. |
| if (Util.isDefinitelyDistinctAndNonNull(nameList)) { |
| return castNonNullList(nameList); |
| } |
| used = new LinkedHashSet<>(); |
| } else { |
| used = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); |
| } |
| int changeCount = 0; |
| final List<String> newNameList = new ArrayList<>(); |
| for (String name : nameList) { |
| String uniqueName = uniquify(name, used, suggester); |
| if (!uniqueName.equals(name)) { |
| ++changeCount; |
| } |
| newNameList.add(uniqueName); |
| } |
| return changeCount == 0 |
| ? castNonNullList(nameList) |
| : newNameList; |
| } |
| |
| /** |
| * Derives the type of a join relational expression. |
| * |
| * @param leftType Row type of left input to join |
| * @param rightType Row type of right input to join |
| * @param joinType Type of join |
| * @param typeFactory Type factory |
| * @param fieldNameList List of names of fields; if null, field names are |
| * inherited and made unique |
| * @param systemFieldList List of system fields that will be prefixed to |
| * output row type; typically empty but must not be |
| * null |
| * @return join type |
| */ |
| public static RelDataType deriveJoinRowType( |
| RelDataType leftType, |
| @Nullable RelDataType rightType, |
| JoinRelType joinType, |
| RelDataTypeFactory typeFactory, |
| @Nullable List<String> fieldNameList, |
| List<RelDataTypeField> systemFieldList) { |
| requireNonNull(systemFieldList, "systemFieldList"); |
| switch (joinType) { |
| case LEFT: |
| rightType = |
| typeFactory.createTypeWithNullability( |
| requireNonNull(rightType, "rightType"), true); |
| break; |
| case RIGHT: |
| leftType = typeFactory.createTypeWithNullability(leftType, true); |
| break; |
| case FULL: |
| leftType = typeFactory.createTypeWithNullability(leftType, true); |
| rightType = |
| typeFactory.createTypeWithNullability( |
| requireNonNull(rightType, "rightType"), true); |
| break; |
| case SEMI: |
| case ANTI: |
| rightType = null; |
| break; |
| default: |
| break; |
| } |
| return createJoinType(typeFactory, leftType, rightType, fieldNameList, |
| systemFieldList); |
| } |
| |
| /** |
| * Returns the type the row which results when two relations are joined. |
| * |
| * <p>The resulting row type consists of |
| * the system fields (if any), followed by |
| * the fields of the left type, followed by |
| * the fields of the right type. The field name list, if present, overrides |
| * the original names of the fields. |
| * |
| * @param typeFactory Type factory |
| * @param leftType Type of left input to join |
| * @param rightType Type of right input to join, or null for semi-join |
| * @param fieldNameList If not null, overrides the original names of the |
| * fields |
| * @param systemFieldList List of system fields that will be prefixed to |
| * output row type; typically empty but must not be |
| * null |
| * @return type of row which results when two relations are joined |
| */ |
| public static RelDataType createJoinType( |
| RelDataTypeFactory typeFactory, |
| RelDataType leftType, |
| @Nullable RelDataType rightType, |
| @Nullable List<String> fieldNameList, |
| List<RelDataTypeField> systemFieldList) { |
| assert (fieldNameList == null) |
| || (fieldNameList.size() |
| == (systemFieldList.size() |
| + leftType.getFieldCount() |
| + (rightType == null ? 0 : rightType.getFieldCount()))); |
| List<String> nameList = new ArrayList<>(); |
| final List<RelDataType> typeList = new ArrayList<>(); |
| |
| // Use a set to keep track of the field names; this is needed |
| // to ensure that the contains() call to check for name uniqueness |
| // runs in constant time; otherwise, if the number of fields is large, |
| // doing a contains() on a list can be expensive. |
| final Set<String> uniqueNameList = |
| typeFactory.getTypeSystem().isSchemaCaseSensitive() |
| ? new HashSet<>() |
| : new TreeSet<>(String.CASE_INSENSITIVE_ORDER); |
| addFields(systemFieldList, typeList, nameList, uniqueNameList); |
| addFields(leftType.getFieldList(), typeList, nameList, uniqueNameList); |
| if (rightType != null) { |
| addFields( |
| rightType.getFieldList(), typeList, nameList, uniqueNameList); |
| } |
| if (fieldNameList != null) { |
| assert fieldNameList.size() == nameList.size(); |
| nameList = fieldNameList; |
| } |
| return typeFactory.createStructType(typeList, nameList); |
| } |
| |
| private static void addFields(List<RelDataTypeField> fieldList, |
| List<RelDataType> typeList, List<String> nameList, |
| Set<String> uniqueNames) { |
| for (RelDataTypeField field : fieldList) { |
| String name = field.getName(); |
| |
| // Ensure that name is unique from all previous field names |
| if (uniqueNames.contains(name)) { |
| String nameBase = name; |
| for (int j = 0;; j++) { |
| name = nameBase + j; |
| if (!uniqueNames.contains(name)) { |
| break; |
| } |
| } |
| } |
| nameList.add(name); |
| uniqueNames.add(name); |
| typeList.add(field.getType()); |
| } |
| } |
| |
| /** |
| * Resolve a target column name in the target table. |
| * |
| * @return the target field or null if the name cannot be resolved |
| * @param rowType the target row type |
| * @param id the target column identifier |
| * @param table the target table or null if it is not a RelOptTable instance |
| */ |
| public static @Nullable RelDataTypeField getTargetField( |
| RelDataType rowType, RelDataTypeFactory typeFactory, |
| SqlIdentifier id, SqlValidatorCatalogReader catalogReader, |
| @Nullable RelOptTable table) { |
| final Table t = table == null ? null : table.unwrap(Table.class); |
| if (!(t instanceof CustomColumnResolvingTable)) { |
| final SqlNameMatcher nameMatcher = catalogReader.nameMatcher(); |
| return nameMatcher.field(rowType, id.getSimple()); |
| } |
| |
| final List<Pair<RelDataTypeField, List<String>>> entries = |
| ((CustomColumnResolvingTable) t).resolveColumn( |
| rowType, typeFactory, id.names); |
| switch (entries.size()) { |
| case 1: |
| if (!entries.get(0).getValue().isEmpty()) { |
| return null; |
| } |
| return entries.get(0).getKey(); |
| default: |
| return null; |
| } |
| } |
| |
| /** |
| * Resolves a multi-part identifier such as "SCHEMA.EMP.EMPNO" to a |
| * namespace. The returned namespace, never null, may represent a |
| * schema, table, column, etc. |
| */ |
| public static SqlValidatorNamespace lookup( |
| SqlValidatorScope scope, |
| List<String> names) { |
| assert names.size() > 0; |
| final SqlNameMatcher nameMatcher = |
| scope.getValidator().getCatalogReader().nameMatcher(); |
| final SqlValidatorScope.ResolvedImpl resolved = |
| new SqlValidatorScope.ResolvedImpl(); |
| scope.resolve(ImmutableList.of(names.get(0)), nameMatcher, false, resolved); |
| assert resolved.count() == 1; |
| SqlValidatorNamespace namespace = resolved.only().namespace; |
| for (String name : Util.skip(names)) { |
| namespace = namespace.lookupChild(name); |
| assert namespace != null; |
| } |
| return namespace; |
| } |
| |
| public static void getSchemaObjectMonikers( |
| SqlValidatorCatalogReader catalogReader, |
| List<String> names, |
| List<SqlMoniker> hints) { |
| // Assume that the last name is 'dummy' or similar. |
| List<String> subNames = Util.skipLast(names); |
| |
| // Try successively with catalog.schema, catalog and no prefix |
| for (List<String> x : catalogReader.getSchemaPaths()) { |
| final List<String> names2 = |
| ImmutableList.<String>builder().addAll(x).addAll(subNames).build(); |
| hints.addAll(catalogReader.getAllSchemaObjectNames(names2)); |
| } |
| } |
| |
| public static @Nullable SelectScope getEnclosingSelectScope(SqlValidatorScope scope) { |
| while (scope instanceof DelegatingScope) { |
| if (scope instanceof SelectScope) { |
| return (SelectScope) scope; |
| } |
| scope = ((DelegatingScope) scope).getParent(); |
| } |
| return null; |
| } |
| |
| public static @Nullable AggregatingSelectScope getEnclosingAggregateSelectScope( |
| SqlValidatorScope scope) { |
| while (scope instanceof DelegatingScope) { |
| if (scope instanceof AggregatingSelectScope) { |
| return (AggregatingSelectScope) scope; |
| } |
| scope = ((DelegatingScope) scope).getParent(); |
| } |
| return null; |
| } |
| |
| /** |
| * Derives the list of column names suitable for NATURAL JOIN. These are the |
| * columns that occur at least once each side of the join. |
| * |
| * @param nameMatcher Whether matches are case-sensitive |
| * @param leftRowType Row type of left input to the join |
| * @param rightRowType Row type of right input to the join |
| * @return List of columns that occur once on each side |
| */ |
| public static List<String> deriveNaturalJoinColumnList( |
| SqlNameMatcher nameMatcher, |
| RelDataType leftRowType, |
| RelDataType rightRowType) { |
| final ImmutableList.Builder<String> naturalColumnNames = |
| ImmutableList.builder(); |
| final Set<String> rightSet = nameMatcher.createSet(); |
| rightSet.addAll(rightRowType.getFieldNames()); |
| final Set<String> leftSet = nameMatcher.createSet(); |
| for (String leftName : leftRowType.getFieldNames()) { |
| if (leftSet.add(leftName) && rightSet.contains(leftName)) { |
| naturalColumnNames.add(leftName); |
| } |
| } |
| return naturalColumnNames.build(); |
| } |
| |
| public static RelDataType createTypeFromProjection(RelDataType type, |
| List<String> columnNameList, RelDataTypeFactory typeFactory, |
| boolean caseSensitive) { |
| // If the names in columnNameList and type have case-sensitive differences, |
| // the resulting type will use those from type. These are presumably more |
| // canonical. |
| final List<RelDataTypeField> fields = |
| new ArrayList<>(columnNameList.size()); |
| for (String name : columnNameList) { |
| RelDataTypeField field = type.getField(name, caseSensitive, false); |
| assert field != null : "field " + name + (caseSensitive ? " (caseSensitive)" : "") |
| + " is not found in " + type; |
| fields.add(type.getFieldList().get(field.getIndex())); |
| } |
| return typeFactory.createStructType(fields); |
| } |
| |
| /** Analyzes an expression in a GROUP BY clause. |
| * |
| * <p>It may be an expression, an empty list (), or a call to |
| * {@code GROUPING SETS}, {@code CUBE}, {@code ROLLUP}, |
| * {@code TUMBLE}, {@code HOP} or {@code SESSION}. |
| * |
| * <p>Each group item produces a list of group sets, which are written to |
| * {@code topBuilder}. To find the grouping sets of the query, we will take |
| * the cartesian product of the group sets. */ |
| public static void analyzeGroupItem(SqlValidatorScope scope, |
| GroupAnalyzer groupAnalyzer, |
| ImmutableList.Builder<ImmutableList<ImmutableBitSet>> topBuilder, |
| SqlNode groupExpr) { |
| final ImmutableList.Builder<ImmutableBitSet> builder; |
| switch (groupExpr.getKind()) { |
| case CUBE: |
| case ROLLUP: |
| // E.g. ROLLUP(a, (b, c)) becomes [{0}, {1, 2}] |
| // then we roll up to [(0, 1, 2), (0), ()] -- note no (0, 1) |
| List<ImmutableBitSet> bitSets = |
| analyzeGroupTuple(scope, groupAnalyzer, |
| ((SqlCall) groupExpr).getOperandList()); |
| switch (groupExpr.getKind()) { |
| case ROLLUP: |
| topBuilder.add(rollup(bitSets)); |
| return; |
| default: |
| topBuilder.add(cube(bitSets)); |
| return; |
| } |
| case OTHER: |
| if (groupExpr instanceof SqlNodeList) { |
| SqlNodeList list = (SqlNodeList) groupExpr; |
| for (SqlNode node : list) { |
| analyzeGroupItem(scope, groupAnalyzer, topBuilder, |
| node); |
| } |
| return; |
| } |
| // fall through |
| case HOP: |
| case TUMBLE: |
| case SESSION: |
| case GROUPING_SETS: |
| default: |
| builder = ImmutableList.builder(); |
| convertGroupSet(scope, groupAnalyzer, builder, |
| groupExpr); |
| topBuilder.add(builder.build()); |
| } |
| } |
| |
| /** Analyzes a GROUPING SETS item in a GROUP BY clause. */ |
| private static void convertGroupSet(SqlValidatorScope scope, |
| GroupAnalyzer groupAnalyzer, |
| ImmutableList.Builder<ImmutableBitSet> builder, SqlNode groupExpr) { |
| switch (groupExpr.getKind()) { |
| case GROUPING_SETS: |
| final SqlCall call = (SqlCall) groupExpr; |
| for (SqlNode node : call.getOperandList()) { |
| convertGroupSet(scope, groupAnalyzer, builder, node); |
| } |
| return; |
| case ROW: |
| final List<ImmutableBitSet> bitSets = |
| analyzeGroupTuple(scope, groupAnalyzer, |
| ((SqlCall) groupExpr).getOperandList()); |
| builder.add(ImmutableBitSet.union(bitSets)); |
| return; |
| case ROLLUP: |
| case CUBE: { |
| // GROUPING SETS ((a), ROLLUP(c,b), CUBE(d,e)) |
| // is EQUIVALENT to |
| // GROUPING SETS ((a), (c,b), (b) ,(), (d,e), (d), (e)). |
| // Expand all ROLLUP/CUBE nodes |
| List<ImmutableBitSet> operandBitSet = |
| analyzeGroupTuple(scope, groupAnalyzer, |
| ((SqlCall) groupExpr).getOperandList()); |
| switch (groupExpr.getKind()) { |
| case ROLLUP: |
| builder.addAll(rollup(operandBitSet)); |
| return; |
| default: |
| builder.addAll(cube(operandBitSet)); |
| return; |
| } |
| } |
| default: |
| builder.add( |
| analyzeGroupExpr(scope, groupAnalyzer, groupExpr)); |
| return; |
| } |
| } |
| |
| /** Analyzes a tuple in a GROUPING SETS clause. |
| * |
| * <p>For example, in {@code GROUP BY GROUPING SETS ((a, b), a, c)}, |
| * {@code (a, b)} is a tuple. |
| * |
| * <p>Gathers into {@code groupExprs} the set of distinct expressions being |
| * grouped, and returns a bitmap indicating which expressions this tuple |
| * is grouping. */ |
| private static List<ImmutableBitSet> analyzeGroupTuple(SqlValidatorScope scope, |
| GroupAnalyzer groupAnalyzer, List<SqlNode> operandList) { |
| List<ImmutableBitSet> list = new ArrayList<>(); |
| for (SqlNode operand : operandList) { |
| list.add( |
| analyzeGroupExpr(scope, groupAnalyzer, operand)); |
| } |
| return list; |
| } |
| |
| /** Analyzes a component of a tuple in a GROUPING SETS clause. */ |
| private static ImmutableBitSet analyzeGroupExpr(SqlValidatorScope scope, |
| GroupAnalyzer groupAnalyzer, |
| SqlNode groupExpr) { |
| final SqlNode expandedGroupExpr = |
| scope.getValidator().expand(groupExpr, scope); |
| |
| switch (expandedGroupExpr.getKind()) { |
| case ROW: |
| return ImmutableBitSet.union( |
| analyzeGroupTuple(scope, groupAnalyzer, |
| ((SqlCall) expandedGroupExpr).getOperandList())); |
| case OTHER: |
| if (expandedGroupExpr instanceof SqlNodeList |
| && ((SqlNodeList) expandedGroupExpr).size() == 0) { |
| return ImmutableBitSet.of(); |
| } |
| break; |
| default: |
| break; |
| } |
| |
| final int ref = lookupGroupExpr(groupAnalyzer, expandedGroupExpr); |
| if (expandedGroupExpr instanceof SqlIdentifier) { |
| // SQL 2003 does not allow expressions of column references |
| SqlIdentifier expr = (SqlIdentifier) expandedGroupExpr; |
| |
| // column references should be fully qualified. |
| assert expr.names.size() >= 2; |
| String originalRelName = expr.names.get(0); |
| String originalFieldName = expr.names.get(1); |
| |
| final SqlNameMatcher nameMatcher = |
| scope.getValidator().getCatalogReader().nameMatcher(); |
| final SqlValidatorScope.ResolvedImpl resolved = |
| new SqlValidatorScope.ResolvedImpl(); |
| scope.resolve(ImmutableList.of(originalRelName), nameMatcher, false, |
| resolved); |
| |
| assert resolved.count() == 1; |
| final SqlValidatorScope.Resolve resolve = resolved.only(); |
| final RelDataType rowType = resolve.rowType(); |
| final int childNamespaceIndex = resolve.path.steps().get(0).i; |
| |
| int namespaceOffset = 0; |
| |
| if (childNamespaceIndex > 0) { |
| // If not the first child, need to figure out the width of |
| // output types from all the preceding namespaces |
| final SqlValidatorScope ancestorScope = resolve.scope; |
| assert ancestorScope instanceof ListScope; |
| List<SqlValidatorNamespace> children = |
| ((ListScope) ancestorScope).getChildren(); |
| |
| for (int j = 0; j < childNamespaceIndex; j++) { |
| namespaceOffset += |
| children.get(j).getRowType().getFieldCount(); |
| } |
| } |
| |
| RelDataTypeField field = |
| requireNonNull(nameMatcher.field(rowType, originalFieldName), |
| () -> "field " + originalFieldName + " is not found in " + rowType |
| + " with " + nameMatcher); |
| int origPos = namespaceOffset + field.getIndex(); |
| |
| groupAnalyzer.groupExprProjection.put(origPos, ref); |
| } |
| |
| return ImmutableBitSet.of(ref); |
| } |
| |
| private static int lookupGroupExpr(GroupAnalyzer groupAnalyzer, |
| SqlNode expr) { |
| for (Ord<SqlNode> node : Ord.zip(groupAnalyzer.groupExprs)) { |
| if (node.e.equalsDeep(expr, Litmus.IGNORE)) { |
| return node.i; |
| } |
| } |
| |
| switch (expr.getKind()) { |
| case HOP: |
| case TUMBLE: |
| case SESSION: |
| groupAnalyzer.extraExprs.add(expr); |
| break; |
| default: |
| break; |
| } |
| groupAnalyzer.groupExprs.add(expr); |
| return groupAnalyzer.groupExprs.size() - 1; |
| } |
| |
| /** Computes the rollup of bit sets. |
| * |
| * <p>For example, <code>rollup({0}, {1})</code> |
| * returns <code>({0, 1}, {0}, {})</code>. |
| * |
| * <p>Bit sets are not necessarily singletons: |
| * <code>rollup({0, 2}, {3, 5})</code> |
| * returns <code>({0, 2, 3, 5}, {0, 2}, {})</code>. */ |
| @VisibleForTesting |
| public static ImmutableList<ImmutableBitSet> rollup( |
| List<ImmutableBitSet> bitSets) { |
| Set<ImmutableBitSet> builder = new LinkedHashSet<>(); |
| for (;;) { |
| final ImmutableBitSet union = ImmutableBitSet.union(bitSets); |
| builder.add(union); |
| if (union.isEmpty()) { |
| break; |
| } |
| bitSets = bitSets.subList(0, bitSets.size() - 1); |
| } |
| return ImmutableList.copyOf(builder); |
| } |
| |
| /** Computes the cube of bit sets. |
| * |
| * <p>For example, <code>rollup({0}, {1})</code> |
| * returns <code>({0, 1}, {0}, {})</code>. |
| * |
| * <p>Bit sets are not necessarily singletons: |
| * <code>rollup({0, 2}, {3, 5})</code> |
| * returns <code>({0, 2, 3, 5}, {0, 2}, {})</code>. */ |
| @VisibleForTesting |
| public static ImmutableList<ImmutableBitSet> cube( |
| List<ImmutableBitSet> bitSets) { |
| // Given the bit sets [{1}, {2, 3}, {5}], |
| // form the lists [[{1}, {}], [{2, 3}, {}], [{5}, {}]]. |
| final Set<List<ImmutableBitSet>> builder = new LinkedHashSet<>(); |
| for (ImmutableBitSet bitSet : bitSets) { |
| builder.add(Arrays.asList(bitSet, ImmutableBitSet.of())); |
| } |
| Set<ImmutableBitSet> flattenedBitSets = new LinkedHashSet<>(); |
| for (List<ImmutableBitSet> o : Linq4j.product(builder)) { |
| flattenedBitSets.add(ImmutableBitSet.union(o)); |
| } |
| return ImmutableList.copyOf(flattenedBitSets); |
| } |
| |
| /** |
| * Finds a {@link org.apache.calcite.jdbc.CalciteSchema.TypeEntry} in a |
| * given schema whose type has the given name, possibly qualified. |
| * |
| * @param rootSchema root schema |
| * @param typeName name of the type, may be qualified or fully-qualified |
| * |
| * @return TypeEntry with a table with the given name, or null |
| */ |
| public static CalciteSchema.@Nullable TypeEntry getTypeEntry( |
| CalciteSchema rootSchema, SqlIdentifier typeName) { |
| final String name; |
| final List<String> path; |
| if (typeName.isSimple()) { |
| path = ImmutableList.of(); |
| name = typeName.getSimple(); |
| } else { |
| path = Util.skipLast(typeName.names); |
| name = Util.last(typeName.names); |
| } |
| CalciteSchema schema = rootSchema; |
| for (String p : path) { |
| if (schema == rootSchema |
| && SqlNameMatchers.withCaseSensitive(true).matches(p, schema.getName())) { |
| continue; |
| } |
| schema = schema.getSubSchema(p, true); |
| if (schema == null) { |
| return null; |
| } |
| } |
| return schema.getType(name, false); |
| } |
| |
| /** |
| * Finds a {@link org.apache.calcite.jdbc.CalciteSchema.TableEntry} in a |
| * given catalog reader whose table has the given name, possibly qualified. |
| * |
| * <p>Uses the case-sensitivity policy of the specified catalog reader. |
| * |
| * <p>If not found, returns null. |
| * |
| * @param catalogReader accessor to the table metadata |
| * @param names Name of table, may be qualified or fully-qualified |
| * |
| * @return TableEntry with a table with the given name, or null |
| */ |
| public static CalciteSchema.@Nullable TableEntry getTableEntry( |
| SqlValidatorCatalogReader catalogReader, List<String> names) { |
| // First look in the default schema, if any. |
| // If not found, look in the root schema. |
| for (List<String> schemaPath : catalogReader.getSchemaPaths()) { |
| CalciteSchema schema = |
| getSchema(catalogReader.getRootSchema(), |
| Iterables.concat(schemaPath, Util.skipLast(names)), |
| catalogReader.nameMatcher()); |
| if (schema == null) { |
| continue; |
| } |
| CalciteSchema.TableEntry entry = |
| getTableEntryFrom(schema, Util.last(names), |
| catalogReader.nameMatcher().isCaseSensitive()); |
| if (entry != null) { |
| return entry; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds and returns {@link CalciteSchema} nested to the given rootSchema |
| * with specified schemaPath. |
| * |
| * <p>Uses the case-sensitivity policy of specified nameMatcher. |
| * |
| * <p>If not found, returns null. |
| * |
| * @param rootSchema root schema |
| * @param schemaPath full schema path of required schema |
| * @param nameMatcher name matcher |
| * |
| * @return CalciteSchema that corresponds specified schemaPath |
| */ |
| public static @Nullable CalciteSchema getSchema(CalciteSchema rootSchema, |
| Iterable<String> schemaPath, SqlNameMatcher nameMatcher) { |
| CalciteSchema schema = rootSchema; |
| for (String schemaName : schemaPath) { |
| if (schema == rootSchema |
| && nameMatcher.matches(schemaName, schema.getName())) { |
| continue; |
| } |
| schema = schema.getSubSchema(schemaName, nameMatcher.isCaseSensitive()); |
| if (schema == null) { |
| return null; |
| } |
| } |
| return schema; |
| } |
| |
| private static CalciteSchema.@Nullable TableEntry getTableEntryFrom( |
| CalciteSchema schema, String name, boolean caseSensitive) { |
| CalciteSchema.TableEntry entry = |
| schema.getTable(name, caseSensitive); |
| if (entry == null) { |
| entry = schema.getTableBasedOnNullaryFunction(name, caseSensitive); |
| } |
| return entry; |
| } |
| |
| /** |
| * Returns whether there are any input columns that are sorted. |
| * |
| * <p>If so, it can be the default ORDER BY clause for a WINDOW specification. |
| * (This is an extension to the SQL standard for streaming.) |
| */ |
| public static boolean containsMonotonic(SqlValidatorScope scope) { |
| for (SqlValidatorNamespace ns : children(scope)) { |
| ns = ns.resolve(); |
| for (String field : ns.getRowType().getFieldNames()) { |
| SqlMonotonicity monotonicity = ns.getMonotonicity(field); |
| if (monotonicity != null && !monotonicity.mayRepeat()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private static List<SqlValidatorNamespace> children(SqlValidatorScope scope) { |
| return scope instanceof ListScope |
| ? ((ListScope) scope).getChildren() |
| : ImmutableList.of(); |
| } |
| |
| /** |
| * Returns whether any of the given expressions are sorted. |
| * |
| * <p>If so, it can be the default ORDER BY clause for a WINDOW specification. |
| * (This is an extension to the SQL standard for streaming.) |
| */ |
| static boolean containsMonotonic(SelectScope scope, SqlNodeList nodes) { |
| for (SqlNode node : nodes) { |
| if (!scope.getMonotonicity(node).mayRepeat()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Lookup sql function by sql identifier and function category. |
| * |
| * @param opTab operator table to look up |
| * @param funName function name |
| * @param funcType function category |
| * @return A sql function if and only if there is one operator matches, else null |
| */ |
| public static @Nullable SqlOperator lookupSqlFunctionByID(SqlOperatorTable opTab, |
| SqlIdentifier funName, |
| @Nullable SqlFunctionCategory funcType) { |
| if (funName.isSimple()) { |
| final List<SqlOperator> list = new ArrayList<>(); |
| opTab.lookupOperatorOverloads(funName, funcType, SqlSyntax.FUNCTION, list, |
| SqlNameMatchers.withCaseSensitive(funName.isComponentQuoted(0))); |
| if (list.size() == 1) { |
| return list.get(0); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Validate the sql node with specified base table row type. For "base table", we mean the |
| * table that the sql node expression references fields with. |
| * |
| * @param caseSensitive whether to match the catalog case-sensitively |
| * @param operatorTable operator table |
| * @param typeFactory type factory |
| * @param rowType the table row type that has fields referenced by the expression |
| * @param expr the expression to validate |
| * @return pair of a validated expression sql node and its data type, |
| * usually a SqlUnresolvedFunction is converted to a resolved function |
| */ |
| public static Pair<SqlNode, RelDataType> validateExprWithRowType( |
| boolean caseSensitive, |
| SqlOperatorTable operatorTable, |
| RelDataTypeFactory typeFactory, |
| RelDataType rowType, |
| SqlNode expr) { |
| final String tableName = "_table_"; |
| final SqlSelect select0 = |
| new SqlSelect(SqlParserPos.ZERO, null, |
| new SqlNodeList(Collections.singletonList(expr), SqlParserPos.ZERO), |
| new SqlIdentifier(tableName, SqlParserPos.ZERO), |
| null, null, null, null, null, null, null, null, null); |
| Prepare.CatalogReader catalogReader = |
| createSingleTableCatalogReader(caseSensitive, tableName, typeFactory, |
| rowType); |
| SqlValidator validator = |
| newValidator(operatorTable, catalogReader, typeFactory, |
| SqlValidator.Config.DEFAULT); |
| final SqlSelect select = (SqlSelect) validator.validate(select0); |
| SqlNodeList selectList = select.getSelectList(); |
| assert selectList.size() == 1 |
| : "Expression " + expr + " should be atom expression"; |
| final SqlNode node = selectList.get(0); |
| final RelDataType nodeType = validator |
| .getValidatedNodeType(select) |
| .getFieldList() |
| .get(0).getType(); |
| return Pair.of(node, nodeType); |
| } |
| |
| /** |
| * Creates a catalog reader that contains a single {@link Table} with temporary table name |
| * and specified {@code rowType}. |
| * |
| * <p>Make this method public so that other systems can also use it. |
| * |
| * @param caseSensitive whether to match case sensitively |
| * @param tableName table name to register with |
| * @param typeFactory type factory |
| * @param rowType table row type |
| * @return the {@link CalciteCatalogReader} instance |
| */ |
| public static CalciteCatalogReader createSingleTableCatalogReader( |
| boolean caseSensitive, |
| String tableName, |
| RelDataTypeFactory typeFactory, |
| RelDataType rowType) { |
| // connection properties |
| Properties properties = new Properties(); |
| properties.put( |
| CalciteConnectionProperty.CASE_SENSITIVE.camelName(), |
| String.valueOf(caseSensitive)); |
| CalciteConnectionConfig connectionConfig = new CalciteConnectionConfigImpl(properties); |
| |
| // prepare root schema |
| final ExplicitRowTypeTable table = new ExplicitRowTypeTable(rowType); |
| final Map<String, Table> tableMap = Collections.singletonMap(tableName, table); |
| CalciteSchema schema = |
| CalciteSchema.createRootSchema(false, false, "", |
| new ExplicitTableSchema(tableMap)); |
| |
| return new CalciteCatalogReader(schema, new ArrayList<>(new ArrayList<>()), |
| typeFactory, connectionConfig); |
| } |
| |
| /** |
| * Flattens an aggregate call. |
| */ |
| public static FlatAggregate flatten(SqlCall call) { |
| return flattenRecurse(null, null, null, call); |
| } |
| |
| private static FlatAggregate flattenRecurse(@Nullable SqlCall filterCall, |
| @Nullable SqlCall distinctCall, @Nullable SqlCall orderCall, |
| SqlCall call) { |
| switch (call.getKind()) { |
| case FILTER: |
| assert filterCall == null; |
| return flattenRecurse(call, distinctCall, orderCall, call.operand(0)); |
| case WITHIN_DISTINCT: |
| assert distinctCall == null; |
| return flattenRecurse(filterCall, call, orderCall, call.operand(0)); |
| case WITHIN_GROUP: |
| assert orderCall == null; |
| return flattenRecurse(filterCall, distinctCall, call, call.operand(0)); |
| default: |
| return new FlatAggregate(call, filterCall, distinctCall, orderCall); |
| } |
| } |
| |
| /** Returns whether a select item is a measure. */ |
| public static boolean isMeasure(SqlNode selectItem) { |
| return getMeasure(selectItem) != null; |
| } |
| |
| /** Returns the measure expression if a select item is a measure, null |
| * otherwise. |
| * |
| * <p>For a measure, {@code selectItem} will have the form |
| * {@code AS(MEASURE(exp), alias)} and this method returns {@code exp}. */ |
| public static @Nullable SqlNode getMeasure(SqlNode selectItem) { |
| // The implementation of this method will be extended when we add the |
| // 'AS MEASURE' construct in [CALCITE-4496]. |
| return null; |
| } |
| |
| /** |
| * When the array element does not equal the array component type, make explicit casting. |
| * |
| * @param componentType derived array component type |
| * @param opBinding description of call |
| */ |
| public static void adjustTypeForArrayConstructor( |
| RelDataType componentType, SqlOperatorBinding opBinding) { |
| if (opBinding instanceof SqlCallBinding) { |
| requireNonNull(componentType, "array component type"); |
| adjustTypeForMultisetConstructor( |
| componentType, componentType, (SqlCallBinding) opBinding); |
| } |
| } |
| |
| /** |
| * When the map key or value does not equal the map component key type or value type, |
| * make explicit casting. |
| * |
| * @param componentType derived map pair component type |
| * @param opBinding description of call |
| */ |
| public static void adjustTypeForMapConstructor( |
| Pair<RelDataType, RelDataType> componentType, SqlOperatorBinding opBinding) { |
| if (opBinding instanceof SqlCallBinding) { |
| requireNonNull(componentType.getKey(), "map key type"); |
| requireNonNull(componentType.getValue(), "map value type"); |
| adjustTypeForMultisetConstructor( |
| componentType.getKey(), componentType.getValue(), (SqlCallBinding) opBinding); |
| } |
| } |
| |
| /** |
| * Adjusts the types for operands in a SqlCallBinding during the construction of a sql collection |
| * type such as Array or Map. This method iterates from the operands of a {@link SqlCall} |
| * obtained from the provided {@link SqlCallBinding}. |
| * It modifies each operand to match the specified 'evenType' or 'oddType' depending on whether |
| * the operand's position is even or odd, respectively. The type adjustment is performed by |
| * explicitly casting each operand to the desired type if the existing operand type does not |
| * match the desired type without considering field names. |
| * If we adjust array, we should set 'evenType' and 'oddType' to same desired type, |
| * if we adjust map, we should set 'evenType' as map key type and 'oddType' as map value type. |
| * |
| * <p>For example, if the operand types are {@code [INT, STRING, INT, STRING]} |
| * with {@code evenType} as {@code BOOLEAN} and {@code oddType} as {@code DOUBLE}, |
| * after executing this method, the types should be {@code [BOOLEAN, DOUBLE, BOOLEAN, DOUBLE]}, |
| * then the corresponding operands are cast to these types. |
| * |
| * @param evenType the {@link RelDataType} to which the operands at even positions should be cast |
| * @param oddType the {@link RelDataType} to which the operands at odd positions should be cast |
| * @param sqlCallBinding the {@link SqlCallBinding} containing the operands to be adjusted |
| */ |
| private static void adjustTypeForMultisetConstructor( |
| RelDataType evenType, RelDataType oddType, SqlCallBinding sqlCallBinding) { |
| SqlCall call = sqlCallBinding.getCall(); |
| List<RelDataType> operandTypes = sqlCallBinding.collectOperandTypes(); |
| List<SqlNode> operands = call.getOperandList(); |
| RelDataType elementType; |
| for (int i = 0; i < operands.size(); i++) { |
| if (i % 2 == 0) { |
| elementType = evenType; |
| } else { |
| elementType = oddType; |
| } |
| if (!operandTypes.get(i).equalsSansFieldNames(elementType)) { |
| call.setOperand(i, castTo(operands.get(i), elementType)); |
| } |
| } |
| } |
| |
| /** |
| * Creates a CAST operation to cast a given {@link SqlNode} to a specified {@link RelDataType}. |
| * This method uses the {@link SqlStdOperatorTable#CAST} operator to create a new {@link SqlCall} |
| * node representing a CAST operation. The original 'node' is cast to the desired 'type', |
| * preserving the nullability of the 'type'. |
| * |
| * @param node the {@link SqlNode} which is to be cast |
| * @param type the target {@link RelDataType} to which 'node' should be cast |
| * @return a new {@link SqlNode} representing the CAST operation |
| */ |
| private static SqlNode castTo(SqlNode node, RelDataType type) { |
| return SqlStdOperatorTable.CAST.createCall( |
| SqlParserPos.ZERO, |
| node, |
| SqlTypeUtil.convertTypeToSpec(type).withNullable(type.isNullable())); |
| } |
| |
| //~ Inner Classes ---------------------------------------------------------- |
| |
| /** |
| * Walks over an expression, copying every node, and fully-qualifying every |
| * identifier. |
| */ |
| @Deprecated // to be removed before 2.0 |
| public static class DeepCopier extends SqlScopedShuttle { |
| DeepCopier(SqlValidatorScope scope) { |
| super(scope); |
| } |
| |
| /** Copies a list of nodes. */ |
| public static @Nullable SqlNodeList copy(SqlValidatorScope scope, SqlNodeList list) { |
| //noinspection deprecation |
| return (@Nullable SqlNodeList) list.accept(new DeepCopier(scope)); |
| } |
| |
| @Override public SqlNode visit(SqlNodeList list) { |
| SqlNodeList copy = new SqlNodeList(list.getParserPosition()); |
| for (SqlNode node : list) { |
| copy.add(node.accept(this)); |
| } |
| return copy; |
| } |
| |
| // Override to copy all arguments regardless of whether visitor changes |
| // them. |
| @Override protected SqlNode visitScoped(SqlCall call) { |
| CallCopyingArgHandler argHandler = |
| new CallCopyingArgHandler(call, true); |
| call.getOperator().acceptCall(this, call, false, argHandler); |
| return argHandler.result(); |
| } |
| |
| @Override public SqlNode visit(SqlLiteral literal) { |
| return SqlNode.clone(literal); |
| } |
| |
| @Override public SqlNode visit(SqlIdentifier id) { |
| // First check for builtin functions which don't have parentheses, |
| // like "LOCALTIME". |
| SqlValidator validator = getScope().getValidator(); |
| final SqlCall call = validator.makeNullaryCall(id); |
| if (call != null) { |
| return call; |
| } |
| |
| return getScope().fullyQualify(id).identifier; |
| } |
| |
| @Override public SqlNode visit(SqlDataTypeSpec type) { |
| return SqlNode.clone(type); |
| } |
| |
| @Override public SqlNode visit(SqlDynamicParam param) { |
| return SqlNode.clone(param); |
| } |
| |
| @Override public SqlNode visit(SqlIntervalQualifier intervalQualifier) { |
| return SqlNode.clone(intervalQualifier); |
| } |
| } |
| |
| /** Suggests candidates for unique names, given the number of attempts so far |
| * and the number of expressions in the project list. */ |
| public interface Suggester { |
| String apply(@Nullable String original, int attempt, int size); |
| } |
| |
| public static final Suggester EXPR_SUGGESTER = |
| (original, attempt, size) -> |
| Util.first(original, SqlUtil.GENERATED_EXPR_ALIAS_PREFIX) + attempt; |
| |
| public static final Suggester F_SUGGESTER = |
| (original, attempt, size) -> Util.first(original, "$f") |
| + Math.max(size, attempt); |
| |
| public static final Suggester ATTEMPT_SUGGESTER = |
| (original, attempt, size) -> Util.first(original, "$") + attempt; |
| |
| /** Builds a list of GROUP BY expressions. */ |
| static class GroupAnalyzer { |
| /** Extra expressions, computed from the input as extra GROUP BY |
| * expressions. For example, calls to the {@code TUMBLE} functions. */ |
| final List<SqlNode> extraExprs = new ArrayList<>(); |
| final List<SqlNode> measureExprs = new ArrayList<>(); |
| final List<SqlNode> groupExprs = new ArrayList<>(); |
| final Map<Integer, Integer> groupExprProjection = new HashMap<>(); |
| final List<ImmutableBitSet> flatGroupSets = new ArrayList<>(); |
| |
| AggregatingSelectScope.Resolved finish() { |
| return new AggregatingSelectScope.Resolved(extraExprs, measureExprs, |
| groupExprs, flatGroupSets, groupExprProjection); |
| } |
| } |
| |
| /** |
| * A {@link AbstractTable} that can specify the row type explicitly. |
| */ |
| private static class ExplicitRowTypeTable extends AbstractTable { |
| private final RelDataType rowType; |
| |
| ExplicitRowTypeTable(RelDataType rowType) { |
| this.rowType = requireNonNull(rowType, "rowType"); |
| } |
| |
| @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) { |
| return this.rowType; |
| } |
| } |
| |
| /** |
| * A {@link AbstractSchema} that can specify the table map explicitly. |
| */ |
| private static class ExplicitTableSchema extends AbstractSchema { |
| private final Map<String, Table> tableMap; |
| |
| ExplicitTableSchema(Map<String, Table> tableMap) { |
| this.tableMap = requireNonNull(tableMap, "tableMap"); |
| } |
| |
| @Override protected Map<String, Table> getTableMap() { |
| return tableMap; |
| } |
| } |
| |
| /** Flattens any FILTER, WITHIN DISTINCT, WITHIN GROUP surrounding a call to |
| * an aggregate function. */ |
| public static class FlatAggregate { |
| public final SqlCall aggregateCall; |
| public final @Nullable SqlCall filterCall; |
| public final @Nullable SqlNode filter; |
| public final @Nullable SqlCall distinctCall; |
| public final @Nullable SqlNodeList distinctList; |
| public final @Nullable SqlCall orderCall; |
| public final @Nullable SqlNodeList orderList; |
| |
| FlatAggregate(SqlCall aggregateCall, @Nullable SqlCall filterCall, |
| @Nullable SqlCall distinctCall, @Nullable SqlCall orderCall) { |
| this.aggregateCall = |
| Objects.requireNonNull(aggregateCall, "aggregateCall"); |
| checkArgument(filterCall == null |
| || filterCall.getKind() == SqlKind.FILTER); |
| checkArgument(distinctCall == null |
| || distinctCall.getKind() == SqlKind.WITHIN_DISTINCT); |
| checkArgument(orderCall == null |
| || orderCall.getKind() == SqlKind.WITHIN_GROUP); |
| this.filterCall = filterCall; |
| this.filter = filterCall == null ? null : filterCall.operand(1); |
| this.distinctCall = distinctCall; |
| this.distinctList = distinctCall == null ? null : distinctCall.operand(1); |
| this.orderCall = orderCall; |
| this.orderList = orderCall == null ? null : orderCall.operand(1); |
| } |
| } |
| } |