All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.jet.sql.impl.validate.HazelcastSqlValidator Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
/*
 * Copyright 2021 Hazelcast Inc.
 *
 * Licensed under the Hazelcast Community License (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://hazelcast.com/hazelcast-community-license
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hazelcast.jet.sql.impl.validate;

import com.hazelcast.jet.sql.impl.connector.SqlConnector;
import com.hazelcast.jet.sql.impl.parse.SqlCreateJob;
import com.hazelcast.jet.sql.impl.parse.SqlCreateMapping;
import com.hazelcast.jet.sql.impl.parse.SqlShowStatement;
import com.hazelcast.jet.sql.impl.schema.HazelcastTable;
import com.hazelcast.jet.sql.impl.schema.JetTableFunction;
import com.hazelcast.jet.sql.impl.validate.literal.LiteralUtils;
import com.hazelcast.jet.sql.impl.validate.param.AbstractParameterConverter;
import com.hazelcast.jet.sql.impl.validate.types.HazelcastTypeCoercion;
import com.hazelcast.jet.sql.impl.validate.types.HazelcastTypeFactory;
import com.hazelcast.jet.sql.impl.validate.types.HazelcastTypeUtils;
import com.hazelcast.sql.impl.ParameterConverter;
import com.hazelcast.sql.impl.QueryException;
import com.hazelcast.sql.impl.SqlErrorCode;
import com.hazelcast.sql.impl.schema.Mapping;
import com.hazelcast.sql.impl.schema.MappingResolver;
import com.hazelcast.sql.impl.schema.Table;
import com.hazelcast.sql.impl.type.QueryDataType;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.runtime.CalciteContextException;
import com.hazelcast.org.apache.calcite.runtime.ResourceUtil;
import com.hazelcast.org.apache.calcite.runtime.Resources;
import com.hazelcast.org.apache.calcite.sql.SqlCall;
import com.hazelcast.org.apache.calcite.sql.SqlDelete;
import com.hazelcast.org.apache.calcite.sql.SqlDynamicParam;
import com.hazelcast.org.apache.calcite.sql.SqlIdentifier;
import com.hazelcast.org.apache.calcite.sql.SqlInsert;
import com.hazelcast.org.apache.calcite.sql.SqlIntervalLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlJoin;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlOperator;
import com.hazelcast.org.apache.calcite.sql.SqlSelect;
import com.hazelcast.org.apache.calcite.sql.SqlUpdate;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserPos;
import com.hazelcast.org.apache.calcite.sql.util.SqlBasicVisitor;
import com.hazelcast.org.apache.calcite.sql.validate.SelectScope;
import com.hazelcast.org.apache.calcite.sql.validate.SqlQualified;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorCatalogReader;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorException;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorImplBridge;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorScope;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorTable;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorUtil;
import com.hazelcast.org.apache.calcite.util.Static;
import com.hazelcast.org.apache.calcite.util.Util;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.hazelcast.jet.sql.impl.connector.SqlConnectorUtil.getJetSqlConnector;
import static com.hazelcast.jet.sql.impl.validate.ValidatorResource.RESOURCE;
import static com.hazelcast.org.apache.calcite.sql.SqlKind.AGGREGATE;
import static com.hazelcast.org.apache.calcite.sql.SqlKind.VALUES;

/**
 * Hazelcast-specific SQL validator.
 */
public class HazelcastSqlValidator extends SqlValidatorImplBridge {

    private static final String OBJECT_NOT_FOUND = ResourceUtil.key(Static.RESOURCE.objectNotFound(""));
    private static final String OBJECT_NOT_FOUND_WITHIN = ResourceUtil.key(Static.RESOURCE.objectNotFoundWithin("", ""));

    private static final Config CONFIG = Config.DEFAULT
            .withIdentifierExpansion(true)
            .withSqlConformance(HazelcastSqlConformance.INSTANCE);

    /** Visitor to rewrite Calcite operators to Hazelcast operators. */
    private final HazelcastSqlOperatorTable.RewriteVisitor rewriteVisitor;

    /** Parameter converter that will be passed to parameter metadata. */
    private final Map parameterConverterMap = new HashMap<>();

    /** Parameter positions. */
    private final Map parameterPositionMap = new HashMap<>();

    /** Parameter values. */
    private final List arguments;

    private final MappingResolver mappingResolver;

    private boolean isCreateJob;
    private boolean isInfiniteRows;

    public HazelcastSqlValidator(
            SqlValidatorCatalogReader catalogReader,
            List arguments,
            MappingResolver mappingResolver
    ) {
        super(HazelcastSqlOperatorTable.instance(), catalogReader, HazelcastTypeFactory.INSTANCE, CONFIG);

        setTypeCoercion(new HazelcastTypeCoercion(this));

        this.rewriteVisitor = new HazelcastSqlOperatorTable.RewriteVisitor(this);
        this.arguments = arguments;
        this.mappingResolver = mappingResolver;
    }

    @Override
    public SqlNode validate(SqlNode topNode) {
        if (topNode instanceof SqlCreateJob) {
            isCreateJob = true;
        }

        if (topNode.getKind().belongsTo(SqlKind.DDL)) {
            topNode.validate(this, getEmptyScope());
            return topNode;
        }

        if (topNode instanceof SqlShowStatement) {
            return topNode;
        }

        return super.validate(topNode);
    }

    @Override
    public void validateQuery(SqlNode node, SqlValidatorScope scope, RelDataType targetRowType) {
        super.validateQuery(node, scope, targetRowType);

        if (node instanceof SqlSelect) {
            validateSelect((SqlSelect) node, scope);
        }
    }

    private void validateSelect(SqlSelect select, SqlValidatorScope scope) {
        // Derive the types for offset-fetch expressions, Calcite doesn't do
        // that automatically.

        SqlNode fetch = select.getFetch();
        if (fetch != null) {
            deriveType(scope, fetch);
            fetch.validate(this, getEmptyScope());
        }

        SqlNode offset = select.getOffset();
        if (offset != null) {
            deriveType(scope, offset);
            offset.validate(this, getEmptyScope());
        }
    }

    @Override
    protected void addToSelectList(
            List list,
            Set aliases,
            List> fieldList,
            SqlNode exp,
            SelectScope scope,
            boolean includeSystemVars
    ) {
        if (isHiddenColumn(exp, scope)) {
            return;
        }

        super.addToSelectList(list, aliases, fieldList, exp, scope, includeSystemVars);
    }

    @Override
    protected void validateFrom(SqlNode node, RelDataType targetRowType, SqlValidatorScope scope) {
        super.validateFrom(node, targetRowType, scope);
        isInfiniteRows = containsStreamingSource(node);
    }

    @Override
    protected void validateGroupClause(SqlSelect select) {
        super.validateGroupClause(select);

        if (containsGroupingOrAggregation(select) && isInfiniteRows(select)) {
            throw newValidationError(select, RESOURCE.streamingAggregationsNotSupported());
        }
    }

    private boolean containsGroupingOrAggregation(SqlSelect select) {
        if (select.getGroup() != null && select.getGroup().size() > 0) {
            return true;
        }

        if (select.isDistinct()) {
            return true;
        }

        for (SqlNode node : select.getSelectList()) {
            if (node.getKind().belongsTo(AGGREGATE)) {
                return true;
            }
        }

        return false;
    }

    @Override
    protected void validateOrderList(SqlSelect select) {
        super.validateOrderList(select);

        if (select.hasOrderBy() && isInfiniteRows(select)) {
            throw newValidationError(select, RESOURCE.streamingSortingNotSupported());
        }
    }

    @Override
    protected void validateJoin(SqlJoin join, SqlValidatorScope scope) {
        super.validateJoin(join, scope);

        // the right side of a join must not be a subquery or a VALUES clause
        join.getRight().accept(new SqlBasicVisitor() {
            @Override
            public Void visit(SqlCall call) {
                if (call.getKind() == SqlKind.SELECT) {
                    throw newValidationError(join, RESOURCE.joiningSubqueryNotSupported());
                } else if (call.getKind() == VALUES) {
                    throw newValidationError(join, RESOURCE.joiningValuesNotSupported());
                }

                return call.getOperator().acceptCall(this, call);
            }
        });
    }

    @Override
    public void validateInsert(SqlInsert insert) {
        super.validateInsert(insert);

        if (!isCreateJob && isInfiniteRows(insert.getSource())) {
            throw newValidationError(insert, RESOURCE.mustUseCreateJob());
        }
    }

    @Override
    protected SqlSelect createSourceSelectForUpdate(SqlUpdate update) {
        SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
        Table table = extractTable((SqlIdentifier) update.getTargetTable());
        if (table != null) {
            SqlConnector connector = getJetSqlConnector(table);

            // only tables with primary keys can be updated
            if (connector.getPrimaryKey(table).isEmpty()) {
                throw QueryException.error("Cannot UPDATE " + update.getTargetTable() + ": it doesn't have a primary key");
            }

            // add all fields, even hidden ones...
            table.getFields().forEach(field -> selectList.add(new SqlIdentifier(field.getName(), SqlParserPos.ZERO)));
        }
        int ordinal = 0;
        for (SqlNode exp : update.getSourceExpressionList()) {
            // Force unique aliases to avoid a duplicate for Y with
            // SET X=Y
            String alias = SqlUtil.deriveAliasFromOrdinal(ordinal);
            selectList.add(SqlValidatorUtil.addAlias(exp, alias));
            ++ordinal;
        }

        SqlNode sourceTable = update.getTargetTable();
        if (update.getAlias() != null) {
            sourceTable = SqlValidatorUtil.addAlias(sourceTable, update.getAlias().getSimple());
        }
        return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
                update.getCondition(), null, null, null, null, null, null, null);
    }

    @Override
    public void validateUpdate(SqlUpdate update) {
        super.validateUpdate(update);

        // hack around Calcite deficiency of not deriving types for fields in sourceExpressionList...
        // see HazelcastTypeCoercion.coerceSourceRowType()
        SqlNodeList selectList = update.getSourceSelect().getSelectList();
        SqlNodeList sourceExpressionList = update.getSourceExpressionList();
        for (int i = 0; i < sourceExpressionList.size(); i++) {
            update.getSourceExpressionList().set(i, selectList.get(selectList.size() - sourceExpressionList.size() + i));
        }

        // UPDATE FROM SELECT is transformed into join (which is not supported yet):
        // UPDATE m1 SET __key = m2.this FROM m2 WHERE m1.__key = m2.__key
        // UPDATE m1 SET __key = (SELECT this FROM m2) WHERE __key = 1
        // UPDATE m1 SET __key = (SELECT m2.this FROM m2 WHERE m1.__key = m2.__key)
        update.getSourceSelect().getSelectList().accept(new SqlBasicVisitor() {
            @Override
            public Void visit(SqlCall call) {
                if (call.getKind() == SqlKind.SELECT) {
                    throw newValidationError(update, RESOURCE.updateFromSelectNotSupported());
                }

                return call.getOperator().acceptCall(this, call);
            }
        });
    }

    @Override
    protected SqlSelect createSourceSelectForDelete(SqlDelete delete) {
        SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
        Table table = extractTable((SqlIdentifier) delete.getTargetTable());
        if (table != null) {
            SqlConnector connector = getJetSqlConnector(table);

            // We need to feed primary keys to the delete processor so that it can directly delete the records.
            // Therefore we use the primary key for the select list.
            connector.getPrimaryKey(table).forEach(name -> selectList.add(new SqlIdentifier(name, SqlParserPos.ZERO)));
            if (selectList.size() == 0) {
                throw QueryException.error("Cannot DELETE from " + delete.getTargetTable() + ": it doesn't have a primary key");
            }
        }

        SqlNode sourceTable = delete.getTargetTable();
        if (delete.getAlias() != null) {
            sourceTable = SqlValidatorUtil.addAlias(sourceTable, delete.getAlias().getSimple());
        }
        return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
                delete.getCondition(), null, null, null, null, null, null, null);
    }

    private Table extractTable(SqlIdentifier identifier) {
        SqlValidatorTable validatorTable = getCatalogReader().getTable(identifier.names);
        return validatorTable == null ? null : validatorTable.unwrap(HazelcastTable.class).getTarget();
    }

    private boolean isInfiniteRows(SqlNode node) {
        isInfiniteRows |= containsStreamingSource(node);
        return isInfiniteRows;
    }

    /**
     * Goes over all the referenced tables in the given {@link SqlNode}
     * and returns true if any of them uses a streaming connector.
     */
    private boolean containsStreamingSource(SqlNode node) {
        class FindStreamingTablesVisitor extends SqlBasicVisitor {
            boolean found;

            @Override
            public Void visit(SqlIdentifier id) {
                SqlValidatorTable table = getCatalogReader().getTable(id.names);
                // not every identifier is a table
                if (table != null) {
                    HazelcastTable hazelcastTable = table.unwrap(HazelcastTable.class);
                    SqlConnector connector = getJetSqlConnector(hazelcastTable.getTarget());
                    if (connector.isStream()) {
                        found = true;
                        return null;
                    }
                }
                return super.visit(id);
            }

            @Override
            public Void visit(SqlCall call) {
                SqlOperator operator = call.getOperator();
                if (operator instanceof JetTableFunction) {
                    if (((JetTableFunction) operator).isStream()) {
                        found = true;
                        return null;
                    }
                }
                return super.visit(call);
            }
        }

        FindStreamingTablesVisitor visitor = new FindStreamingTablesVisitor();
        node.accept(visitor);
        return visitor.found;
    }

    @Override
    public RelDataType deriveTypeImpl(SqlValidatorScope scope, SqlNode operand) {
        if (operand.getKind() == SqlKind.LITERAL) {
            RelDataType literalType = LiteralUtils.literalType(operand, (HazelcastTypeFactory) typeFactory);

            if (literalType != null) {
                return literalType;
            }
        }

        return super.deriveTypeImpl(scope, operand);
    }

    @Override
    public void validateLiteral(SqlLiteral literal) {
        if (literal instanceof SqlIntervalLiteral) {
            super.validateLiteral(literal);
        }

        // Disable validation of other literals
    }

    @Override
    public void validateDynamicParam(SqlDynamicParam dynamicParam) {
        parameterPositionMap.put(dynamicParam.getIndex(), dynamicParam.getParserPosition());
    }

    @Override
    public void validateCall(SqlCall call, SqlValidatorScope scope) {
        // Enforce type derivation for all calls before validation. Calcite may
        // skip it if a call has a fixed type, for instance AND always has
        // BOOLEAN type, so operands may end up having no validated type.
        deriveType(scope, call);
        super.validateCall(call, scope);
    }

    @Override
    protected SqlNode performUnconditionalRewrites(SqlNode node, boolean underFrom) {
        SqlNode rewritten = super.performUnconditionalRewrites(node, underFrom);

        if (rewritten != null && rewritten.isA(SqlKind.TOP_LEVEL)) {
            // Rewrite operators to Hazelcast ones starting at every top node.
            // For instance, SELECT a + b is rewritten to SELECT a + b, where
            // the first '+' refers to the standard Calcite SqlStdOperatorTable.PLUS
            // operator and the second '+' refers to HazelcastSqlOperatorTable.PLUS
            // operator.
            rewritten.accept(rewriteVisitor);
        }

        return rewritten;
    }

    @Override
    public HazelcastTypeCoercion getTypeCoercion() {
        return (HazelcastTypeCoercion) super.getTypeCoercion();
    }

    public void setParameterConverter(int ordinal, ParameterConverter parameterConverter) {
        parameterConverterMap.put(ordinal, parameterConverter);
    }

    public Object getArgumentAt(int index) {
        ParameterConverter parameterConverter = parameterConverterMap.get(index);
        Object argument = arguments.get(index);
        return parameterConverter.convert(argument);
    }

    public ParameterConverter[] getParameterConverters(SqlNode node) {
        // Get original parameter row type.
        RelDataType rowType = getParameterRowType(node);

        // Create precedence-based converters with optional override by a more specialized converters.
        ParameterConverter[] res = new ParameterConverter[rowType.getFieldCount()];

        for (int i = 0; i < res.length; i++) {
            ParameterConverter converter = parameterConverterMap.get(i);

            if (converter == null) {
                QueryDataType targetType =
                        HazelcastTypeUtils.toHazelcastType(rowType.getFieldList().get(i).getType().getSqlTypeName());
                converter = AbstractParameterConverter.from(targetType, i, parameterPositionMap.get(i));
            }

            res[i] = converter;
        }

        return res;
    }

    private boolean isHiddenColumn(SqlNode node, SelectScope scope) {
        if (!(node instanceof SqlIdentifier)) {
            return false;
        }

        SqlIdentifier identifier = (SqlIdentifier) node;

        String fieldName = extractFieldName(identifier, scope);

        if (fieldName == null) {
            return false;
        }

        SqlValidatorTable table = scope.fullyQualify(identifier).namespace.getTable();

        if (table == null) {
            return false;
        }

        HazelcastTable unwrappedTable = table.unwrap(HazelcastTable.class);

        if (unwrappedTable == null) {
            return false;
        }

        return unwrappedTable.isHidden(fieldName);
    }

    private String extractFieldName(SqlIdentifier identifier, SelectScope scope) {
        SqlCall call = makeNullaryCall(identifier);

        if (call != null) {
            return null;
        }

        SqlQualified qualified = scope.fullyQualify(identifier);

        List names = qualified.identifier.names;

        if (names.size() < 2) {
            return null;
        }

        return Util.last(names);
    }

    /**
     * Returns whether the validated node returns an infinite number of rows.
     *
     * @throws IllegalStateException if called before the node is validated.
     */
    public boolean isInfiniteRows() {
        return isInfiniteRows;
    }

    @Override
    public CalciteContextException newValidationError(SqlNode node, Resources.ExInst e) {
        assert node != null;

        CalciteContextException exception = SqlUtil.newContextException(node.getParserPosition(), e);
        if (OBJECT_NOT_FOUND.equals(ResourceUtil.key(e)) || OBJECT_NOT_FOUND_WITHIN.equals(ResourceUtil.key(e))) {
            Object[] arguments = ResourceUtil.args(e);
            String identifier = (arguments != null && arguments.length > 0) ? String.valueOf(arguments[0]) : null;
            Mapping mapping = identifier != null ? mappingResolver.resolve(identifier) : null;
            String sql = mapping != null ? SqlCreateMapping.unparse(mapping) : null;
            String message = sql != null ? ValidatorResource.imapNotMapped(e.str(), identifier, sql) : e.str();
            throw QueryException.error(SqlErrorCode.OBJECT_NOT_FOUND, message, exception, sql);
        }
        return exception;
    }
}