/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.voltdb.planner; import java.net.URL; import java.util.EmptyStackException; import java.util.List; import java.util.Stack; import org.apache.commons.lang3.StringUtils; import org.voltdb.catalog.Catalog; import org.voltdb.catalog.Database; import org.voltdb.compiler.DeterminismMode; import org.voltdb.expressions.AbstractExpression; import org.voltdb.expressions.TupleValueExpression; import org.voltdb.plannodes.AbstractPlanNode; import org.voltdb.plannodes.NestLoopPlanNode; import org.voltdb.plannodes.OrderByPlanNode; import org.voltdb.plannodes.SeqScanPlanNode; import org.voltdb.types.ExpressionType; import org.voltdb.types.JoinType; import org.voltdb.types.PlanNodeType; import org.voltdb.types.SortDirectionType; import junit.framework.TestCase; public class PlannerTestCase extends TestCase { private PlannerTestAideDeCamp m_aide; private boolean m_byDefaultInferPartitioning = true; private boolean m_byDefaultPlanForSinglePartition; final private int m_defaultParamCount = 0; private String m_noJoinOrder = null; /** * @param sql * @return */ private int countQuestionMarks(String sql) { int paramCount = 0; int skip = 0; while (true) { // Yes, we ARE assuming that test queries don't contain quoted question marks. skip = sql.indexOf('?', skip); if (skip == -1) { break; } skip++; paramCount++; } return paramCount; } protected void failToCompile(String sql, String... patterns) { int paramCount = countQuestionMarks(sql); try { List<AbstractPlanNode> unexpected = m_aide.compile(sql, paramCount, m_byDefaultInferPartitioning, m_byDefaultPlanForSinglePartition, null); printExplainPlan(unexpected); fail("Expected planner failure, but found success."); } catch (Exception ex) { String result = ex.toString(); for (String pattern : patterns) { if ( ! result.contains(pattern)) { fail("Did not find pattern '" + pattern + "' in error string '" + result + "'"); } } } } protected CompiledPlan compileAdHocPlan(String sql) { return compileAdHocPlan(sql, DeterminismMode.SAFER); } protected CompiledPlan compileAdHocPlan(String sql, DeterminismMode detMode) { CompiledPlan cp = null; try { cp = m_aide.compileAdHocPlan(sql, detMode); assertTrue(cp != null); } catch (Exception ex) { ex.printStackTrace(); fail(ex.getMessage()); } return cp; } /** * Fetch compiled planned based on provided partitioning information. * @param sql: SQL statement * @param inferPartitioning: Flag to indicate whether to use infer or forced partitioning * when generating plan. True to use infer partitioning info, * false for forced partitioning * @param forcedSP: Flag to indicate whether to generate plan for forced SP or MP. * If inferPartitioing flag is set to true, this flag is ignored * @param detMode: Specifies determinism mode - Faster or Safer * @return: Compiled plan based on specified input parameters */ protected CompiledPlan compileAdHocPlan(String sql, boolean inferPartitioning, boolean forcedSP, DeterminismMode detMode) { CompiledPlan cp = null; try { cp = m_aide.compileAdHocPlan(sql, inferPartitioning, forcedSP, detMode); assertTrue(cp != null); } catch (Exception ex) { ex.printStackTrace(); fail(); } return cp; } protected CompiledPlan compileAdHocPlan(String sql, boolean inferPartitioning, boolean forcedSP) { return compileAdHocPlan(sql, inferPartitioning, forcedSP, DeterminismMode.SAFER); } protected List<AbstractPlanNode> compileInvalidToFragments(String sql) { boolean planForSinglePartitionFalse = false; return compileWithJoinOrderToFragments(sql, m_defaultParamCount, planForSinglePartitionFalse, m_noJoinOrder); } /** A helper here where the junit test can assert success */ protected List<AbstractPlanNode> compileToFragments(String sql) { boolean planForSinglePartitionFalse = false; return compileWithJoinOrderToFragments(sql, planForSinglePartitionFalse, m_noJoinOrder); } protected List<AbstractPlanNode> compileToFragmentsForSinglePartition(String sql) { boolean planForSinglePartitionFalse = false; return compileWithJoinOrderToFragments(sql, planForSinglePartitionFalse, m_noJoinOrder); } /** A helper here where the junit test can assert success */ protected List<AbstractPlanNode> compileWithJoinOrderToFragments(String sql, String joinOrder) { boolean planForSinglePartitionFalse = false; return compileWithJoinOrderToFragments(sql, planForSinglePartitionFalse, joinOrder); } /** A helper here where the junit test can assert success */ private List<AbstractPlanNode> compileWithJoinOrderToFragments(String sql, boolean planForSinglePartition, String joinOrder) { try { // Yes, we ARE assuming that test queries don't contain quoted question marks. int paramCount = StringUtils.countMatches(sql, "?"); return compileWithJoinOrderToFragments(sql, paramCount, planForSinglePartition, joinOrder); } catch (PlanningErrorException pe) { fail("Query: '" + sql + "' threw " + pe); return null; // dead code. } } /** A helper here where the junit test can assert success */ private List<AbstractPlanNode> compileWithJoinOrderToFragments(String sql, int paramCount, boolean planForSinglePartition, String joinOrder) { //* enable to debug */ System.out.println("DEBUG: compileWithJoinOrderToFragments(\"" + sql + "\", " + planForSinglePartition + ", \"" + joinOrder + "\")"); List<AbstractPlanNode> pn = m_aide.compile(sql, paramCount, m_byDefaultInferPartitioning, m_byDefaultPlanForSinglePartition, joinOrder); assertTrue(pn != null); assertFalse(pn.isEmpty()); assertTrue(pn.get(0) != null); if (planForSinglePartition) { assertTrue(pn.size() == 1); } return pn; } protected AbstractPlanNode compileSPWithJoinOrder(String sql, String joinOrder) { try { return compileWithCountedParamsAndJoinOrder(sql, joinOrder); } catch (Exception ex) { ex.printStackTrace(); fail(); return null; } } protected void compileWithInvalidJoinOrder(String sql, String joinOrder) throws Exception { compileWithJoinOrderToFragments(sql, m_defaultParamCount, m_byDefaultPlanForSinglePartition, joinOrder); } private AbstractPlanNode compileWithCountedParamsAndJoinOrder(String sql, String joinOrder) throws Exception { // Yes, we ARE assuming that test queries don't contain quoted question marks. int paramCount = StringUtils.countMatches(sql, "?"); return compileSPWithJoinOrder(sql, paramCount, joinOrder); } /** * Assert that the plan for a statement produces a plan that meets some * basic expectations. * @param sql a statement to plan * as if for a single-partition stored procedure * @param nOutputColumns the expected number of plan result columns, * because of the planner's history of such errors * @param nodeTypes the expected node types of the resulting plan tree * listed in top-down order with wildcard support. * See assertTopDownTree. * @return the plan for more detailed testing. */ protected AbstractPlanNode compileToTopDownTree(String sql, int nOutputColumns, PlanNodeType... nodeTypes) { // Yes, we ARE assuming that test queries don't // contain quoted question marks. int paramCount = StringUtils.countMatches(sql, "?"); AbstractPlanNode result = compileSPWithJoinOrder(sql, paramCount, null); assertEquals(nOutputColumns, result.getOutputSchema().size()); assertTopDownTree(result, nodeTypes); return result; } /** A helper here where the junit test can assert success */ protected AbstractPlanNode compile(String sql) { // Yes, we ARE assuming that test queries don't contain quoted question marks. int paramCount = StringUtils.countMatches(sql, "?"); return compileSPWithJoinOrder(sql, paramCount, null); } /** A helper here where the junit test can assert success */ protected AbstractPlanNode compileForSinglePartition(String sql) { // Yes, we ARE assuming that test queries don't contain quoted question marks. int paramCount = StringUtils.countMatches(sql, "?"); boolean m_infer = m_byDefaultInferPartitioning; boolean m_forceSP = m_byDefaultInferPartitioning; m_byDefaultInferPartitioning = false; m_byDefaultPlanForSinglePartition = true; AbstractPlanNode pn = compileSPWithJoinOrder(sql, paramCount, null); m_byDefaultInferPartitioning = m_infer; m_byDefaultPlanForSinglePartition = m_forceSP; return pn; } /** A helper here where the junit test can assert success */ protected AbstractPlanNode compileSPWithJoinOrder(String sql, int paramCount, String joinOrder) { List<AbstractPlanNode> pns = null; try { pns = compileWithJoinOrderToFragments(sql, paramCount, m_byDefaultPlanForSinglePartition, joinOrder); } catch (Exception ex) { ex.printStackTrace(); fail(ex.getMessage()); } assertTrue(pns.get(0) != null); return pns.get(0); } /** * Find all the aggregate nodes in a fragment, whether they are hash, serial or partial. * @param fragment Fragment to search for aggregate plan nodes * @return a list of all the nodes we found */ protected static List<AbstractPlanNode> findAllAggPlanNodes(AbstractPlanNode fragment) { List<AbstractPlanNode> aggNodes = fragment.findAllNodesOfType(PlanNodeType.AGGREGATE); List<AbstractPlanNode> hashAggNodes = fragment.findAllNodesOfType(PlanNodeType.HASHAGGREGATE); List<AbstractPlanNode> partialAggNodes = fragment.findAllNodesOfType(PlanNodeType.PARTIALAGGREGATE); aggNodes.addAll(hashAggNodes); aggNodes.addAll(partialAggNodes); return aggNodes; } protected void setupSchema(URL ddlURL, String basename, boolean planForSinglePartition) throws Exception { m_aide = new PlannerTestAideDeCamp(ddlURL, basename); m_byDefaultPlanForSinglePartition = planForSinglePartition; } protected void setupSchema(boolean inferPartitioning, URL ddlURL, String basename) throws Exception { m_byDefaultInferPartitioning = inferPartitioning; m_aide = new PlannerTestAideDeCamp(ddlURL, basename); } public String getCatalogString() { return m_aide.getCatalogString(); } public Catalog getCatalog() { return m_aide.getCatalog(); } Database getDatabase() { return m_aide.getDatabase(); } protected void printExplainPlan(List<AbstractPlanNode> planNodes) { for (AbstractPlanNode apn: planNodes) { System.out.println(apn.toExplainPlanString()); } } protected String buildExplainPlan(List<AbstractPlanNode> planNodes) { String explain = ""; for (AbstractPlanNode apn: planNodes) { explain += apn.toExplainPlanString() + '\n'; } return explain; } protected void checkQueriesPlansAreTheSame(String sql1, String sql2) { String explainStr1, explainStr2; List<AbstractPlanNode> pns = compileToFragments(sql1); explainStr1 = buildExplainPlan(pns); pns = compileToFragments(sql2); explainStr2 = buildExplainPlan(pns); assertEquals(explainStr1, explainStr2); } /** * Call this function to verify that an order by plan node has the * sort expressions and directions we expect. * * @param orderByPlanNode The plan node to test. * @param columnDescrs Pairs of expressions and sort directions. There * must be an even number of these, the even * numbered ones must be expressions and the odd * numbered ones must be sort directions. This is * numbering starting at 0. So, they must be in * the order expr, direction, expr, direction, and * so forth. */ protected void verifyOrderByPlanNode(OrderByPlanNode orderByPlanNode, Object ... columnDescrs) { // We should have an even number of columns assertEquals(0, columnDescrs.length % 2); List<AbstractExpression> exprs = orderByPlanNode.getSortExpressions(); List<SortDirectionType> dirs = orderByPlanNode.getSortDirections(); assertEquals(exprs.size(), dirs.size()); assertEquals(columnDescrs.length/2, exprs.size()); for (int idx = 0; idx < exprs.size(); ++idx) { // Assert that an expected one-part name matches a tve by column name // and an expected two-part name matches a tve by table and column name. AbstractExpression expr = exprs.get(idx); assertTrue(expr instanceof TupleValueExpression); TupleValueExpression tve = (TupleValueExpression)expr; String expectedNames[] = ((String)columnDescrs[2*idx]).split("\\."); String columnName = null; int nParts = expectedNames.length; if (nParts > 1) { assertEquals(2, nParts); String tableName = expectedNames[0].toUpperCase(); assertEquals(tableName, tve.getTableName().toUpperCase()); } // In either case, the column name must match the LAST part. columnName = expectedNames[nParts-1].toUpperCase(); assertEquals(columnName, tve.getColumnName().toUpperCase()); SortDirectionType dir = dirs.get(idx); assertEquals(columnDescrs[2*idx+1], dir); } } /** * Assert that a plan's left-most branch is made up of plan nodes of * specified classes. * @param expectedClasses a list of expected AbstractPlanNode classes * @param actualPlan the top of a plan node tree expected to have instances * of the expected classes along its left-most branch * listed from top to bottom. */ static protected void assertClassesMatchNodeChain( List<Class<? extends AbstractPlanNode>> expectedClasses, AbstractPlanNode actualPlan) { AbstractPlanNode pn = actualPlan; for (Class<? extends AbstractPlanNode> c : expectedClasses) { assertFalse("The actual plan is shallower than expected", pn == null); assertTrue("Expected plan to contain an instance of " + c.getSimpleName() +", " + "instead found " + pn.getClass().getSimpleName(), c.isInstance(pn)); if (pn.getChildCount() > 0) { pn = pn.getChild(0); } else { pn = null; } } assertTrue("Actual plan longer than expected", pn == null); } /** * Find a specific node in a plan tree following the left-most path, * (child[0]), and asserting the expected class of each plan node along the * way, inclusive of the start and end. * @param expectedClasses a list of expected AbstractPlanNode classes * @param actualPlan the top of a plan node tree expected to have instances * of the expected classes along its left-most branch * listed in top-down order. * @return the child node matching the last expected class in the list. * It need not be a leaf node. */ protected static AbstractPlanNode followAssertedLeftChain( AbstractPlanNode start, PlanNodeType startType, PlanNodeType... nodeTypes) { AbstractPlanNode result = start; assertEquals(startType, result.getPlanNodeType()); for (PlanNodeType type : nodeTypes) { assertTrue(result.getChildCount() > 0); result = result.getChild(0); assertEquals(type, result.getPlanNodeType()); } return result; } /** * Assert that a plan's left-most branch is made up of plan nodes of * expected classes. * @param expectedClasses a list of expected AbstractPlanNode classes * @param actualPlan the top of a plan node tree expected to have instances * of the expected classes along its left-most branch * listed from top to bottom. */ protected static void assertLeftChain( AbstractPlanNode start, PlanNodeType... nodeTypes) { AbstractPlanNode pn = start; for (PlanNodeType type : nodeTypes) { assertFalse("Child node(s) are missing from the actual plan chain.", pn == null); if ( ! type.equals(pn.getPlanNodeType())) { fail("Expecting plan node of type " + type + ", " + "instead found " + pn.getPlanNodeType() + "."); } pn = (pn.getChildCount() > 0) ? pn.getChild(0) : null; } assertTrue("Actual plan chain was longer than expected", pn == null); } /** * Assert that a two-fragment plan's coordinator fragment does a simple * projection. **/ protected static void assertProjectingCoordinator( List<AbstractPlanNode> lpn) { AbstractPlanNode pn; pn = lpn.get(0); assertTopDownTree(pn, PlanNodeType.SEND, PlanNodeType.PROJECTION, PlanNodeType.RECEIVE); } /** * Assert that a two-fragment plan's coordinator fragment does a left join * with a specific replicated table on its outer side. **/ protected static void assertReplicatedLeftJoinCoordinator( List<AbstractPlanNode> lpn, String replicatedTable) { AbstractPlanNode pn; AbstractPlanNode node; NestLoopPlanNode nlj; SeqScanPlanNode seqScan; pn = lpn.get(0); assertTopDownTree(pn, PlanNodeType.SEND, PlanNodeType.PROJECTION, PlanNodeType.NESTLOOP, PlanNodeType.SEQSCAN, PlanNodeType.RECEIVE); node = followAssertedLeftChain(pn, PlanNodeType.SEND, PlanNodeType.PROJECTION, PlanNodeType.NESTLOOP); nlj = (NestLoopPlanNode) node; assertEquals(JoinType.LEFT, nlj.getJoinType()); assertEquals(2, nlj.getChildCount()); seqScan = (SeqScanPlanNode) nlj.getChild(0); assertEquals(replicatedTable, seqScan.getTargetTableName().toUpperCase()); } // Print a tree of plan nodes by type. protected void printPlanNodes(AbstractPlanNode root, int fragmentNumber, int numberOfFragments) { System.out.printf(" Plan for fragment %d of %d\n", fragmentNumber, numberOfFragments); String lines[] = root.toExplainPlanString().split("\n"); System.out.printf(" Explain:\n"); for (String line : lines) { System.out.printf(" %s\n", line); } System.out.printf(" Nodes:\n"); for (;root != null; root = (root.getChildCount() > 0) ? root.getChild(0) : null) { System.out.printf(" Node type %s\n", root.getPlanNodeType()); for (int idx = 1; idx < root.getChildCount(); idx += 1) { System.out.printf(" Child %d: %s\n", idx, root.getChild(idx).getPlanNodeType()); } } } /** * Assert that an expression tree contains the expected types of expressions * in the order listed, assuming a top-down left-to-right depth-first * traversal through left, right, and args children. * A null expression type in the list will match any expression * node or tree at the corresponding position. **/ protected static void assertExprTopDownTree(AbstractExpression start, ExpressionType... exprTypes) { assertNotNull(start); Stack<AbstractExpression> stack = new Stack<>(); stack.push(start); for (ExpressionType type : exprTypes) { // Process each node before its children or later siblings. AbstractExpression parent; try { parent = stack.pop(); } catch (EmptyStackException ese) { fail("No expression was found in the tree to match type " + type); return; // This dead code hushes warnings. } List<AbstractExpression> args = parent.getArgs(); AbstractExpression rightExpr = parent.getRight(); AbstractExpression leftExpr = parent.getLeft(); int argCount = (args == null) ? 0 : args.size(); int childCount = argCount + (rightExpr == null ? 0 : 1) + (leftExpr == null ? 0 : 1); if (type == null) { // A null type wildcard matches any child TREE or NODE. System.out.println("DEBUG: Suggestion -- expect " + parent.getExpressionType() + " with " + childCount + " direct children."); continue; } assertEquals(type, parent.getExpressionType()); // Iterate from the last child to the first. while (argCount > 0) { // Push each child to be processed before its parent's // or its own later siblings (already pushed). stack.push(parent.getArgs().get(--argCount)); } if (rightExpr != null) { stack.push(rightExpr); } if (leftExpr != null) { stack.push(leftExpr); } } assertTrue("Extra expression node(s) (" + stack.size() + ") were found in the tree with no expression type to match", stack.isEmpty()); } /** * Assert that a plan node tree contains the expected types of plan nodes * in the order listed, assuming a top-down left-to-right depth-first * traversal through the child vector. A null plan node type in the list * will match any plan node or subtree at the corresponding position. **/ protected static void assertTopDownTree(AbstractPlanNode start, PlanNodeType... nodeTypes) { Stack<AbstractPlanNode> stack = new Stack<>(); stack.push(start); for (PlanNodeType type : nodeTypes) { // Process each node before its children or later siblings. AbstractPlanNode parent; try { parent = stack.pop(); } catch (EmptyStackException ese) { fail("No node was found in the tree to match node type " + type); return; // This dead code hushes warnings. } int childCount = parent.getChildCount(); if (type == null) { // A null type wildcard matches any child TREE or NODE. System.out.println("DEBUG: Suggestion -- expect " + parent.getPlanNodeType() + " with " + childCount + " direct children."); continue; } assertEquals(type, parent.getPlanNodeType()); // Iterate from the last child to the first. while (childCount > 0) { // Push each child to be processed before its parent's // or its own later (already pushed) siblings. stack.push(parent.getChild(--childCount)); } } assertTrue("Extra plan node(s) (" + stack.size() + ") were found in the tree with no node type to match", stack.isEmpty()); } /** * Validate a plan, ignoring inline nodes. This is kind of like * PlannerTestCase.compileToTopDownTree. The differences are * <ol> * <li>We only look out out-of-line nodes,</li> * <li>We can compile MP plans and SP plans, and</li> * <li>The boundaries between fragments in MP plans * are marked with PlanNodeType.INVALID.</li> * <li>We can describe inline nodes pretty easily.</li> * </ol> * * See TestWindowFunctions.testWindowFunctionWithIndex for examples * of the use of this function. * * @param SQL The statement text. * @param numberOfFragments The number of expected fragments. * @param types The plan node types of the inline and out-of-line nodes. * If types[idx] is a PlanNodeType, then the node should * have no inline children. If types[idx] is an array of * PlanNodeType values then the node has the type types[idx][0], * and it should have types[idx][1..] as inline children. */ protected void validatePlan(String SQL, int numberOfFragments, Object ...types) { List<AbstractPlanNode> fragments = compileToFragments(SQL); assertEquals(String.format("Expected %d fragments, not %d", numberOfFragments, fragments.size()), numberOfFragments, fragments.size()); int idx = 0; int fragment = 1; // The index of the last PlanNodeType in types. int nchildren = types.length; System.out.printf("Plan for <%s>\n", SQL); for (AbstractPlanNode plan : fragments) { printPlanNodes(plan, fragment, numberOfFragments); // The boundaries between fragments are // marked with PlanNodeType.INVALID. if (fragment > 1) { assertEquals("Expected a fragment to start here", PlanNodeType.INVALID, types[idx]); idx += 1; } fragment += 1; for (;plan != null; idx += 1) { if (types.length <= idx) { fail(String.format("Expected %d plan nodes, but found more.", types.length)); } if (types[idx] instanceof PlanNodeType) { assertEquals(types[idx], plan.getPlanNodeType()); } else if (types[idx] instanceof PlanNodeType[]) { PlanNodeType childTypes[] = (PlanNodeType[])(types[idx]); assertEquals(childTypes[0], plan.getPlanNodeType()); for (int tidx = 1; tidx < childTypes.length; tidx += 1) { PlanNodeType childType = childTypes[tidx]; assertTrue(String.format("Expected inline node of type %s", childType), plan.getInlinePlanNode(childType) != null); } } else { fail("Expected a PlanNodeType or an array of PlanNodeTypes here."); } plan = (plan.getChildCount() > 0) ? plan.getChild(0) : null; } } assertEquals(nchildren, idx); } }