/** * 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.tajo.plan.verifier; import com.google.common.base.Preconditions; import org.apache.tajo.BuiltinStorages; import org.apache.tajo.OverridableConf; import org.apache.tajo.SessionVars; import org.apache.tajo.TajoConstants; import org.apache.tajo.algebra.*; import org.apache.tajo.catalog.CatalogService; import org.apache.tajo.catalog.TableDesc; import org.apache.tajo.exception.*; import org.apache.tajo.plan.algebra.BaseAlgebraVisitor; import org.apache.tajo.plan.util.ExprFinder; import org.apache.tajo.schema.IdentifierUtil; import org.apache.tajo.validation.ConstraintViolation; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.Stack; import static org.apache.tajo.plan.verifier.SyntaxErrorUtil.makeSyntaxError; public class PreLogicalPlanVerifier extends BaseAlgebraVisitor<PreLogicalPlanVerifier.Context, Expr> { private CatalogService catalog; public PreLogicalPlanVerifier(CatalogService catalog) { this.catalog = catalog; } public static class Context { OverridableConf queryContext; VerificationState state; public Context(OverridableConf queryContext, VerificationState state) { this.queryContext = queryContext; this.state = state; } } public VerificationState verify(OverridableConf queryContext, VerificationState state, Expr expr) throws TajoException { Context context = new Context(queryContext, state); visit(context, new Stack<>(), expr); return context.state; } @Override public Expr visitSetSession(Context ctx, Stack<Expr> stack, SetSession expr) throws TajoException { // we should allow undefined session variables which can be used in query statements in the future. if (SessionVars.exists(expr.getName())) { SessionVars var = SessionVars.get(expr.getName()); if (var.validator() != null) { Collection<ConstraintViolation> violations = var.validator().validate(expr.getValue()); for (ConstraintViolation violation : violations) { ctx.state.addVerification(SyntaxErrorUtil.makeInvalidSessionVar(var.keyname(), violation.getMessage())); } } } return expr; } public Expr visitProjection(Context context, Stack<Expr> stack, Projection expr) throws TajoException { super.visitProjection(context, stack, expr); Set<String> names = new HashSet<>(); for (NamedExpr namedExpr : expr.getNamedExprs()) { if (namedExpr.hasAlias()) { if (names.contains(namedExpr.getAlias())) { context.state.addVerification(SyntaxErrorUtil.makeDuplicateAlias(namedExpr.getAlias())); } else { names.add(namedExpr.getAlias()); } } } return expr; } @Override public Expr visitLimit(Context context, Stack<Expr> stack, Limit expr) throws TajoException { stack.push(expr); if (ExprFinder.finds(expr.getFetchFirstNum(), OpType.Column).size() > 0) { context.state.addVerification(SyntaxErrorUtil.makeSyntaxError("argument of LIMIT must not contain variables")); } visit(context, stack, expr.getFetchFirstNum()); Expr result = visit(context, stack, expr.getChild()); stack.pop(); return result; } @Override public Expr visitGroupBy(Context context, Stack<Expr> stack, Aggregation expr) throws TajoException { super.visitGroupBy(context, stack, expr); // Enforcer only ordinary grouping set. for (Aggregation.GroupElement groupingElement : expr.getGroupSet()) { if (groupingElement.getType() != Aggregation.GroupType.OrdinaryGroup) { context.state.addVerification(ExceptionUtil.makeNotSupported(groupingElement.getType().name())); } } Projection projection = null; for (Expr parent : stack) { if (parent.getType() == OpType.Projection) { projection = (Projection) parent; break; } } if (projection == null) { throw new TajoInternalError("No Projection"); } return expr; } @Override public Expr visitRelation(Context context, Stack<Expr> stack, Relation expr) throws TajoException { assertRelationExistence(context, expr.getName()); return expr; } private boolean assertRelationExistence(Context context, String tableName) { String qualifiedName; if (IdentifierUtil.isFQTableName(tableName)) { qualifiedName = tableName; } else { qualifiedName = IdentifierUtil.buildFQName(context.queryContext.get(SessionVars.CURRENT_DATABASE), tableName); } if (!catalog.existsTable(qualifiedName)) { context.state.addVerification(new UndefinedTableException(qualifiedName)); return false; } return true; } private void assertRelationSchema(Context context, CreateTable createTable) { for (ColumnDefinition colDef : createTable.getTableElements()) { if (colDef.isMapType()) { context.state.addVerification(new UnsupportedException("map type")); } } } private static String guessTableName(Context context, String givenName) { String qualifiedName; if (IdentifierUtil.isFQTableName(givenName)) { qualifiedName = givenName; } else { qualifiedName = IdentifierUtil.buildFQName(context.queryContext.get(SessionVars.CURRENT_DATABASE), givenName); } return qualifiedName; } private boolean assertRelationNoExistence(Context context, String tableName) { String qualifiedName = guessTableName(context, tableName); if (catalog.existsTable(qualifiedName)) { context.state.addVerification(new DuplicateTableException(qualifiedName)); return false; } return true; } private boolean assertSupportedDataFormat(VerificationState state, String name) { Preconditions.checkNotNull(name); if (name.equalsIgnoreCase("RAW")) { state.addVerification(SyntaxErrorUtil.makeUnknownDataFormat(name)); return false; } return true; } private boolean assertDatabaseExistence(VerificationState state, String name) { if (!catalog.existDatabase(name)) { state.addVerification(new UndefinedDatabaseException(name)); return false; } return true; } private boolean assertDatabaseNoExistence(VerificationState state, String name) { if (catalog.existDatabase(name)) { state.addVerification(new DuplicateDatabaseException(name)); return false; } return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // Data Definition Language Section /////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public Expr visitCreateDatabase(Context context, Stack<Expr> stack, CreateDatabase expr) throws TajoException { super.visitCreateDatabase(context, stack, expr); if (!expr.isIfNotExists()) { assertDatabaseNoExistence(context.state, expr.getDatabaseName()); } return expr; } @Override public Expr visitDropDatabase(Context context, Stack<Expr> stack, DropDatabase expr) throws TajoException { super.visitDropDatabase(context, stack, expr); if (!expr.isIfExists()) { assertDatabaseExistence(context.state, expr.getDatabaseName()); } return expr; } @Override public Expr visitCreateTable(Context context, Stack<Expr> stack, CreateTable expr) throws TajoException { super.visitCreateTable(context, stack, expr); if (!expr.isIfNotExists()) { assertRelationNoExistence(context, expr.getTableName()); } if (expr.hasTableElements()) { assertRelationSchema(context, expr); } else { if (expr.getStorageType() != null) { if (expr.hasSelfDescSchema()) { // TODO: support other types like Parquet and ORC. if (!expr.getStorageType().equalsIgnoreCase(BuiltinStorages.JSON) && !expr.getStorageType().equalsIgnoreCase(BuiltinStorages.EX_HTTP_JSON)) { if (expr.getStorageType().equalsIgnoreCase(BuiltinStorages.PARQUET) || expr.getStorageType().equalsIgnoreCase(BuiltinStorages.ORC)) { throw new NotImplementedException(expr.getStorageType()); } else { throw new UnsupportedException(expr.getStorageType()); } } } else { if (expr.getLikeParentTableName() == null && expr.getSubQuery() == null) { throw new TajoInternalError(expr.getTableName() + " does not have pre-defined or self-describing schema"); } } } } if (expr.hasStorageType()) { assertSupportedDataFormat(context.state, expr.getStorageType()); } return expr; } @Override public Expr visitDropTable(Context context, Stack<Expr> stack, DropTable expr) throws TajoException { super.visitDropTable(context, stack, expr); if (!expr.isIfExists()) { assertRelationExistence(context, expr.getTableName()); } return expr; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // Insert or Update Section /////////////////////////////////////////////////////////////////////////////////////////////////////////// public Expr visitInsert(Context context, Stack<Expr> stack, Insert expr) throws TajoException { Expr child = super.visitInsert(context, stack, expr); if (expr.hasTableName()) { assertRelationExistence(context, expr.getTableName()); } if (expr.hasStorageType()) { assertSupportedDataFormat(context.state, expr.getDataFormat()); } if (child != null && child.getType() == OpType.Projection) { Projection projection = (Projection) child; // checking if at least one asterisk exists in target list boolean includeAsterisk = false; for (NamedExpr namedExpr : projection.getNamedExprs()) { includeAsterisk |= namedExpr.getExpr().getType() == OpType.Asterisk; } // If one asterisk expression exists, we verify the match between the target exprs and output exprs. // This verification will be in LogicalPlanVerifier. if (!includeAsterisk) { int projectColumnNum = projection.getNamedExprs().size(); if (expr.hasTargetColumns()) { int targetColumnNum = expr.getTargetColumns().length; if (targetColumnNum > projectColumnNum) { context.state.addVerification(makeSyntaxError("INSERT has more target columns than expressions")); } else if (targetColumnNum < projectColumnNum) { context.state.addVerification(makeSyntaxError("INSERT has more expressions than target columns")); } } else { if (expr.hasTableName()) { String qualifiedName = expr.getTableName(); if (TajoConstants.EMPTY_STRING.equals(IdentifierUtil.extractQualifier(expr.getTableName()))) { qualifiedName = IdentifierUtil.buildFQName(context.queryContext.get(SessionVars.CURRENT_DATABASE), expr.getTableName()); } TableDesc table = catalog.getTableDesc(qualifiedName); if (table == null) { context.state.addVerification(new UndefinedTableException(qualifiedName)); return null; } if (table.hasPartition()) { int columnSize = table.getSchema().getRootColumns().size(); columnSize += table.getPartitionMethod().getExpressionSchema().getRootColumns().size(); if (projectColumnNum < columnSize) { context.state.addVerification(makeSyntaxError("INSERT has smaller expressions than target columns")); } else if (projectColumnNum > columnSize) { context.state.addVerification(makeSyntaxError("INSERT has more expressions than target columns")); } } } } } } return expr; } // TODO: This should be removed at TAJO-1891 @Override public Expr visitAlterTable(Context context, Stack<Expr> stack, AlterTable expr) throws TajoException { if (expr.getAlterTableOpType() == AlterTableOpType.ADD_PARTITION) { context.state.addVerification(new NotImplementedException("ADD PARTITION")); } return expr; } }