static void compileSingleStmtProcedure( VoltCompiler compiler, HSQLInterface hsql, DatabaseEstimates estimates, Catalog catalog, Database db, ProcedureDescriptor procedureDescriptor) throws VoltCompiler.VoltCompilerException { final String className = procedureDescriptor.m_className; if (className.indexOf('@') != -1) { throw compiler.new VoltCompilerException("User procedure names can't contain \"@\"."); } // get the short name of the class (no package if a user procedure) // use the Table.<builtin> name (allowing the period) if builtin. String shortName = className; if (procedureDescriptor.m_builtInStmt == false) { String[] parts = className.split("\\."); shortName = parts[parts.length - 1]; } // add an entry to the catalog (using the full className) final Procedure procedure = db.getProcedures().add(shortName); for (String groupName : procedureDescriptor.m_authGroups) { final Group group = db.getGroups().get(groupName); if (group == null) { throw compiler .new VoltCompilerException( "Procedure " + className + " allows access by a role " + groupName + " that does not exist"); } final GroupRef groupRef = procedure.getAuthgroups().add(groupName); groupRef.setGroup(group); } procedure.setClassname(className); // sysprocs don't use the procedure compiler procedure.setSystemproc(false); procedure.setDefaultproc(procedureDescriptor.m_builtInStmt); procedure.setHasjava(false); // get the annotation // first try to get one that has been passed from the compiler ProcInfoData info = compiler.getProcInfoOverride(shortName); // then check for the usual one in the class itself // and create a ProcInfo.Data instance for it if (info == null) { info = new ProcInfoData(); if (procedureDescriptor.m_partitionString != null) { info.partitionInfo = procedureDescriptor.m_partitionString; info.singlePartition = true; } } assert (info != null); // ADD THE STATEMENT // add the statement to the catalog Statement catalogStmt = procedure.getStatements().add(VoltDB.ANON_STMT_NAME); // compile the statement StatementPartitioning partitioning = info.singlePartition ? StatementPartitioning.forceSP() : StatementPartitioning.forceMP(); // default to FASTER detmode because stmt procs can't feed read output into writes StatementCompiler.compileFromSqlTextAndUpdateCatalog( compiler, hsql, catalog, db, estimates, catalogStmt, procedureDescriptor.m_singleStmt, procedureDescriptor.m_joinOrder, DeterminismMode.FASTER, partitioning); // if the single stmt is not read only, then the proc is not read only boolean procHasWriteStmts = (catalogStmt.getReadonly() == false); // set the read onlyness of a proc procedure.setReadonly(procHasWriteStmts == false); int seqs = catalogStmt.getSeqscancount(); procedure.setHasseqscans(seqs > 0); // set procedure parameter types CatalogMap<ProcParameter> params = procedure.getParameters(); CatalogMap<StmtParameter> stmtParams = catalogStmt.getParameters(); // set the procedure parameter types from the statement parameter types int paramCount = 0; for (StmtParameter stmtParam : CatalogUtil.getSortedCatalogItems(stmtParams, "index")) { // name each parameter "param1", "param2", etc... ProcParameter procParam = params.add("param" + String.valueOf(paramCount)); procParam.setIndex(stmtParam.getIndex()); procParam.setIsarray(stmtParam.getIsarray()); procParam.setType(stmtParam.getJavatype()); paramCount++; } // parse the procinfo procedure.setSinglepartition(info.singlePartition); if (info.singlePartition) { parsePartitionInfo(compiler, db, procedure, info.partitionInfo); if (procedure.getPartitionparameter() >= params.size()) { String msg = "PartitionInfo parameter not a valid parameter for procedure: " + procedure.getClassname(); throw compiler.new VoltCompilerException(msg); } // TODO: The planner does not currently validate that a single-statement plan declared as // single-partition correctly uses // the designated parameter as a partitioning filter, maybe some day. // In theory, the PartitioningForStatement would confirm the use of (only) a parameter as a // partition key -- // or if the partition key was determined to be some other hard-coded constant (expression?) // it might display a warning // message that the passed parameter is assumed to be equal to that constant (expression). } else { if (partitioning.getCountOfIndependentlyPartitionedTables() == 1) { AbstractExpression statementPartitionExpression = partitioning.singlePartitioningExpressionForReport(); if (statementPartitionExpression != null) { // The planner has uncovered an overlooked opportunity to run the statement SP. String msg = null; if (statementPartitionExpression instanceof ParameterValueExpression) { msg = "This procedure would benefit from setting the attribute 'partitioninfo=" + partitioning.getFullColumnName() + ":" + ((ParameterValueExpression) statementPartitionExpression).getParameterIndex() + "'"; } else { String valueDescription = null; Object partitionValue = partitioning.getInferredPartitioningValue(); if (partitionValue == null) { // Statement partitioned on a runtime constant. This is likely to be cryptic, but // hopefully gets the idea across. valueDescription = "of " + statementPartitionExpression.explain(""); } else { valueDescription = partitionValue.toString(); // A simple constant value COULD have been a parameter. } msg = "This procedure would benefit from adding a parameter to be passed the value " + valueDescription + " and setting the attribute 'partitioninfo=" + partitioning.getFullColumnName() + ":" + paramCount + "'"; } compiler.addWarn(msg); } } } }
static String generateProcedureRow(Procedure procedure) { StringBuilder sb = new StringBuilder(); sb.append("<tr class='primaryrow'>"); // column 1: procedure name String anchor = procedure.getTypeName().toLowerCase(); sb.append( "<td style='white-space: nowrap'><i id='p-" + anchor + "--icon' class='icon-chevron-right'></i> <a href='#p-"); sb.append(anchor).append("' id='p-").append(anchor).append("' class='togglex'>"); sb.append(procedure.getTypeName()); sb.append("</a></td>"); // column 2: parameter types sb.append("<td>"); List<ProcParameter> params = CatalogUtil.getSortedCatalogItems(procedure.getParameters(), "index"); List<String> paramTypes = new ArrayList<String>(); for (ProcParameter param : params) { String paramType = VoltType.get((byte) param.getType()).name(); if (param.getIsarray()) { paramType += "[]"; } paramTypes.add(paramType); } if (paramTypes.size() == 0) { sb.append("<i>None</i>"); } sb.append(StringUtils.join(paramTypes, ", ")); sb.append("</td>"); // column 3: partitioning sb.append("<td>"); if (procedure.getSinglepartition()) { tag(sb, "success", "Single"); } else { tag(sb, "warning", "Multi"); } sb.append("</td>"); // column 4: read/write sb.append("<td>"); if (procedure.getReadonly()) { tag(sb, "success", "Read"); } else { tag(sb, "warning", "Write"); } sb.append("</td>"); // column 5: access sb.append("<td>"); List<String> groupNames = new ArrayList<String>(); for (GroupRef groupRef : procedure.getAuthgroups()) { groupNames.add(groupRef.getGroup().getTypeName()); } if (groupNames.size() == 0) { sb.append("<i>None</i>"); } sb.append(StringUtils.join(groupNames, ", ")); sb.append("</td>"); // column 6: attributes sb.append("<td>"); if (procedure.getHasjava()) { tag(sb, "info", "Java"); } else { tag(sb, null, "Single-Stmt"); } boolean isND = false; int scanCount = 0; for (Statement stmt : procedure.getStatements()) { scanCount += stmt.getSeqscancount(); if (!stmt.getIscontentdeterministic() || !stmt.getIsorderdeterministic()) { isND = false; } } if (isND) { tag(sb, "inverse", "Determinism"); } if (scanCount > 0) { tag(sb, "important", "Scans"); } sb.append("</td>"); sb.append("</tr>\n"); // BUILD THE DROPDOWN FOR THE STATEMENT/DETAIL TABLE sb.append( "<tr class='tablesorter-childRow'><td class='invert' colspan='6' id='p-" + procedure.getTypeName().toLowerCase() + "--dropdown'>\n"); // output partitioning parameter info if (procedure.getSinglepartition()) { String pTable = procedure.getPartitioncolumn().getParent().getTypeName(); String pColumn = procedure.getPartitioncolumn().getTypeName(); int pIndex = procedure.getPartitionparameter(); sb.append( String.format( "<p>Partitioned on parameter %d which maps to column %s" + " of table <a class='invert' href='#s-%s'>%s</a>.</p>", pIndex, pColumn, pTable, pTable)); } // output what schema this interacts with ProcedureAnnotation annotation = (ProcedureAnnotation) procedure.getAnnotation(); if (annotation != null) { // make sure tables appear in only one category annotation.tablesRead.removeAll(annotation.tablesUpdated); if (annotation.tablesRead.size() > 0) { sb.append("<p>Read-only access to tables: "); List<String> tables = new ArrayList<String>(); for (Table table : annotation.tablesRead) { tables.add("<a href='#s-" + table.getTypeName() + "'>" + table.getTypeName() + "</a>"); } sb.append(StringUtils.join(tables, ", ")); sb.append("</p>"); } if (annotation.tablesUpdated.size() > 0) { sb.append("<p>Read/Write access to tables: "); List<String> tables = new ArrayList<String>(); for (Table table : annotation.tablesUpdated) { tables.add("<a href='#s-" + table.getTypeName() + "'>" + table.getTypeName() + "</a>"); } sb.append(StringUtils.join(tables, ", ")); sb.append("</p>"); } if (annotation.indexesUsed.size() > 0) { sb.append("<p>Uses indexes: "); List<String> indexes = new ArrayList<String>(); for (Index index : annotation.indexesUsed) { Table table = (Table) index.getParent(); indexes.add( "<a href='#s-" + table.getTypeName() + "-" + index.getTypeName() + "'>" + index.getTypeName() + "</a>"); } sb.append(StringUtils.join(indexes, ", ")); sb.append("</p>"); } } sb.append(generateStatementsTable(procedure)); sb.append("</td></tr>\n"); return sb.toString(); }
static void compileJavaProcedure( VoltCompiler compiler, HSQLInterface hsql, DatabaseEstimates estimates, Catalog catalog, Database db, ProcedureDescriptor procedureDescriptor, InMemoryJarfile jarOutput) throws VoltCompiler.VoltCompilerException { final String className = procedureDescriptor.m_className; final Language lang = procedureDescriptor.m_language; // Load the class given the class name Class<?> procClass = procedureDescriptor.m_class; // get the short name of the class (no package) String shortName = deriveShortProcedureName(className); // add an entry to the catalog final Procedure procedure = db.getProcedures().add(shortName); for (String groupName : procedureDescriptor.m_authGroups) { final Group group = db.getGroups().get(groupName); if (group == null) { throw compiler .new VoltCompilerException( "Procedure " + className + " allows access by a role " + groupName + " that does not exist"); } final GroupRef groupRef = procedure.getAuthgroups().add(groupName); groupRef.setGroup(group); } procedure.setClassname(className); // sysprocs don't use the procedure compiler procedure.setSystemproc(false); procedure.setDefaultproc(procedureDescriptor.m_builtInStmt); procedure.setHasjava(true); procedure.setLanguage(lang.name()); ProcedureAnnotation pa = (ProcedureAnnotation) procedure.getAnnotation(); if (pa == null) { pa = new ProcedureAnnotation(); procedure.setAnnotation(pa); } if (procedureDescriptor.m_scriptImpl != null) { // This is a Groovy or other Java derived procedure and we need to add an annotation with // the script to the Procedure element in the Catalog pa.scriptImpl = procedureDescriptor.m_scriptImpl; } // get the annotation // first try to get one that has been passed from the compiler ProcInfoData info = compiler.getProcInfoOverride(shortName); // check if partition info was set in ddl ProcInfoData ddlInfo = null; if (procedureDescriptor.m_partitionString != null && !procedureDescriptor.m_partitionString.trim().isEmpty()) { ddlInfo = new ProcInfoData(); ddlInfo.partitionInfo = procedureDescriptor.m_partitionString; ddlInfo.singlePartition = true; } // then check for the usual one in the class itself // and create a ProcInfo.Data instance for it if (info == null) { info = new ProcInfoData(); ProcInfo annotationInfo = procClass.getAnnotation(ProcInfo.class); // error out if partition info is present in both ddl and annotation if (annotationInfo != null) { if (ddlInfo != null) { String msg = "Procedure: " + shortName + " has partition properties defined both in "; msg += "class \"" + className + "\" and in the schema defintion file(s)"; throw compiler.new VoltCompilerException(msg); } // Prevent AutoGenerated DDL from including PARTITION PROCEDURE for this procedure. pa.classAnnotated = true; info.partitionInfo = annotationInfo.partitionInfo(); info.singlePartition = annotationInfo.singlePartition(); } else if (ddlInfo != null) { info = ddlInfo; } } else { pa.classAnnotated = true; } assert (info != null); // make sure multi-partition implies no partitoning info if (info.singlePartition == false) { if ((info.partitionInfo != null) && (info.partitionInfo.length() > 0)) { String msg = "Procedure: " + shortName + " is annotated as multi-partition"; msg += " but partitionInfo has non-empty value: \"" + info.partitionInfo + "\""; throw compiler.new VoltCompilerException(msg); } } // track if there are any writer statements and/or sequential scans and/or an overlooked common // partitioning parameter boolean procHasWriteStmts = false; boolean procHasSeqScans = false; // procWantsCommonPartitioning == true but commonPartitionExpression == null signifies a proc // for which the planner was requested to attempt to find an SP plan, but that was not possible // -- it had a replicated write or it had one or more partitioned reads that were not all // filtered by the same partition key value -- so it was planned as an MP proc. boolean procWantsCommonPartitioning = true; AbstractExpression commonPartitionExpression = null; String exampleSPstatement = null; Object exampleSPvalue = null; // iterate through the fields and get valid sql statements Map<String, Object> fields = lang.accept(procedureIntrospector(compiler), procClass); // determine if proc is read or read-write by checking if the proc contains any write sql stmts boolean readWrite = false; for (Object field : fields.values()) { if (!(field instanceof SQLStmt)) continue; SQLStmt stmt = (SQLStmt) field; QueryType qtype = QueryType.getFromSQL(stmt.getText()); if (!qtype.isReadOnly()) { readWrite = true; break; } } // default to FASTER determinism mode, which may favor non-deterministic plans // but if it's a read-write proc, use a SAFER planning mode wrt determinism. final DeterminismMode detMode = readWrite ? DeterminismMode.SAFER : DeterminismMode.FASTER; for (Entry<String, Object> entry : fields.entrySet()) { if (!(entry.getValue() instanceof SQLStmt)) continue; String stmtName = entry.getKey(); SQLStmt stmt = (SQLStmt) entry.getValue(); // add the statement to the catalog Statement catalogStmt = procedure.getStatements().add(stmtName); // compile the statement StatementPartitioning partitioning = info.singlePartition ? StatementPartitioning.forceSP() : StatementPartitioning.forceMP(); boolean cacheHit = StatementCompiler.compileFromSqlTextAndUpdateCatalog( compiler, hsql, catalog, db, estimates, catalogStmt, stmt.getText(), stmt.getJoinOrder(), detMode, partitioning); // if this was a cache hit or specified single, don't worry about figuring out more // partitioning if (partitioning.wasSpecifiedAsSingle() || cacheHit) { procWantsCommonPartitioning = false; // Don't try to infer what's already been asserted. // The planner does not currently attempt to second-guess a plan declared as // single-partition, maybe some day. // In theory, the PartitioningForStatement would confirm the use of (only) a parameter as a // partition key -- // or if the partition key was determined to be some other constant (expression?) it might // display an informational // message that the passed parameter is assumed to be equal to the hard-coded partition key // constant (expression). // Validate any inferred statement partitioning given the statement's possible usage, until // a contradiction is found. } else if (procWantsCommonPartitioning) { // Only consider statements that are capable of running SP with a partitioning parameter // that does not seem to // conflict with the partitioning of prior statements. if (partitioning.getCountOfIndependentlyPartitionedTables() == 1) { AbstractExpression statementPartitionExpression = partitioning.singlePartitioningExpressionForReport(); if (statementPartitionExpression != null) { if (commonPartitionExpression == null) { commonPartitionExpression = statementPartitionExpression; exampleSPstatement = stmt.getText(); exampleSPvalue = partitioning.getInferredPartitioningValue(); } else if (commonPartitionExpression.equals(statementPartitionExpression) || (statementPartitionExpression instanceof ParameterValueExpression && commonPartitionExpression instanceof ParameterValueExpression)) { // Any constant used for partitioning would have to be the same for all statements, // but // any statement parameter used for partitioning MIGHT come from the same proc // parameter as // any other statement's parameter used for partitioning. } else { procWantsCommonPartitioning = false; // appears to be different partitioning for different statements } } else { // There is a statement with a partitioned table whose partitioning column is // not equality filtered with a constant or param. Abandon all hope. procWantsCommonPartitioning = false; } // Usually, replicated-only statements in a mix with others have no effect on the MP/SP // decision } else if (partitioning.getCountOfPartitionedTables() == 0) { // but SP is strictly forbidden for DML, to maintain the consistency of the replicated // data. if (partitioning.getIsReplicatedTableDML()) { procWantsCommonPartitioning = false; } } else { // There is a statement with a partitioned table whose partitioning column is // not equality filtered with a constant or param. Abandon all hope. procWantsCommonPartitioning = false; } } // if a single stmt is not read only, then the proc is not read only if (catalogStmt.getReadonly() == false) { procHasWriteStmts = true; } if (catalogStmt.getSeqscancount() > 0) { procHasSeqScans = true; } } // MIGHT the planner have uncovered an overlooked opportunity to run all statements SP? if (procWantsCommonPartitioning && (commonPartitionExpression != null)) { String msg = null; if (commonPartitionExpression instanceof ParameterValueExpression) { msg = "This procedure might benefit from an @ProcInfo annotation designating parameter " + ((ParameterValueExpression) commonPartitionExpression).getParameterIndex() + " of statement '" + exampleSPstatement + "'"; } else { String valueDescription = null; if (exampleSPvalue == null) { // Statements partitioned on a runtime constant. This is likely to be cryptic, but // hopefully gets the idea across. valueDescription = "of " + commonPartitionExpression.explain(""); } else { valueDescription = exampleSPvalue.toString(); // A simple constant value COULD have been a parameter. } msg = "This procedure might benefit from an @ProcInfo annotation referencing an added parameter passed the value " + valueDescription; } compiler.addInfo(msg); } // set the read onlyness of a proc procedure.setReadonly(procHasWriteStmts == false); procedure.setHasseqscans(procHasSeqScans); for (Statement catalogStmt : procedure.getStatements()) { if (catalogStmt.getIscontentdeterministic() == false) { String potentialErrMsg = "Procedure " + shortName + " has a statement with a non-deterministic result - statement: \"" + catalogStmt.getSqltext() + "\" , reason: " + catalogStmt.getNondeterminismdetail(); // throw compiler.new VoltCompilerException(potentialErrMsg); compiler.addWarn(potentialErrMsg); } else if (catalogStmt.getIsorderdeterministic() == false) { String warnMsg; if (procHasWriteStmts) { String rwPotentialErrMsg = "Procedure " + shortName + " is RW and has a statement whose result has a non-deterministic ordering - statement: \"" + catalogStmt.getSqltext() + "\", reason: " + catalogStmt.getNondeterminismdetail(); // throw compiler.new VoltCompilerException(rwPotentialErrMsg); warnMsg = rwPotentialErrMsg; } else { warnMsg = "Procedure " + shortName + " has a statement with a non-deterministic result - statement: \"" + catalogStmt.getSqltext() + "\", reason: " + catalogStmt.getNondeterminismdetail(); } compiler.addWarn(warnMsg); } } // set procedure parameter types CatalogMap<ProcParameter> params = procedure.getParameters(); Class<?>[] paramTypes = lang.accept(procedureEntryPointParametersTypeExtractor, fields); for (int i = 0; i < paramTypes.length; i++) { Class<?> cls = paramTypes[i]; ProcParameter param = params.add(String.valueOf(i)); param.setIndex(i); // handle the case where the param is an array if (cls.isArray()) { param.setIsarray(true); cls = cls.getComponentType(); } else param.setIsarray(false); // boxed types are not supported parameters at this time if ((cls == Long.class) || (cls == Integer.class) || (cls == Short.class) || (cls == Byte.class) || (cls == Double.class) || (cls == Character.class) || (cls == Boolean.class)) { String msg = "Procedure: " + shortName + " has a parameter with a boxed type: "; msg += cls.getSimpleName(); msg += ". Replace this parameter with the corresponding primitive type and the procedure may compile."; throw compiler.new VoltCompilerException(msg); } else if ((cls == Float.class) || (cls == float.class)) { String msg = "Procedure: " + shortName + " has a parameter with type: "; msg += cls.getSimpleName(); msg += ". Replace this parameter type with double and the procedure may compile."; throw compiler.new VoltCompilerException(msg); } VoltType type; try { type = VoltType.typeFromClass(cls); } catch (VoltTypeException e) { // handle the case where the type is invalid String msg = "Procedure: " + shortName + " has a parameter with invalid type: "; msg += cls.getSimpleName(); throw compiler.new VoltCompilerException(msg); } catch (RuntimeException e) { String msg = "Procedure: " + shortName + " unexpectedly failed a check on a parameter of type: "; msg += cls.getSimpleName(); msg += " with error: "; msg += e.toString(); throw compiler.new VoltCompilerException(msg); } param.setType(type.getValue()); } // parse the procinfo procedure.setSinglepartition(info.singlePartition); if (info.singlePartition) { parsePartitionInfo(compiler, db, procedure, info.partitionInfo); if (procedure.getPartitionparameter() >= paramTypes.length) { String msg = "PartitionInfo parameter not a valid parameter for procedure: " + procedure.getClassname(); throw compiler.new VoltCompilerException(msg); } // check the type of partition parameter meets our high standards Class<?> partitionType = paramTypes[procedure.getPartitionparameter()]; Class<?>[] validPartitionClzzes = { Long.class, Integer.class, Short.class, Byte.class, long.class, int.class, short.class, byte.class, String.class, byte[].class }; boolean found = false; for (Class<?> candidate : validPartitionClzzes) { if (partitionType == candidate) found = true; } if (!found) { String msg = "PartitionInfo parameter must be a String or Number for procedure: " + procedure.getClassname(); throw compiler.new VoltCompilerException(msg); } VoltType columnType = VoltType.get((byte) procedure.getPartitioncolumn().getType()); VoltType paramType = VoltType.typeFromClass(partitionType); if (!columnType.canExactlyRepresentAnyValueOf(paramType)) { String msg = "Type mismatch between partition column and partition parameter for procedure " + procedure.getClassname() + " may cause overflow or loss of precision.\nPartition column is type " + columnType + " and partition parameter is type " + paramType; throw compiler.new VoltCompilerException(msg); } else if (!paramType.canExactlyRepresentAnyValueOf(columnType)) { String msg = "Type mismatch between partition column and partition parameter for procedure " + procedure.getClassname() + " does not allow the full range of partition key values.\nPartition column is type " + columnType + " and partition parameter is type " + paramType; compiler.addWarn(msg); } } // put the compiled code for this procedure into the jarfile // need to find the outermost ancestor class for the procedure in the event // that it's actually an inner (or inner inner...) class. // addClassToJar recursively adds all the children, which should include this // class Class<?> ancestor = procClass; while (ancestor.getEnclosingClass() != null) { ancestor = ancestor.getEnclosingClass(); } compiler.addClassToJar(jarOutput, ancestor); }
static String genrateStatementRow(Procedure procedure, Statement statement) { StringBuilder sb = new StringBuilder(); sb.append(" <tr class='primaryrow2'>"); // name column String anchor = (procedure.getTypeName() + "-" + statement.getTypeName()).toLowerCase(); sb.append( "<td style='white-space: nowrap'><i id='p-" + anchor + "--icon' class='icon-chevron-right'></i> <a href='#' id='p-"); sb.append(anchor).append("' class='togglex'>"); sb.append(statement.getTypeName()); sb.append("</a></td>"); // sql column sb.append("<td><tt>"); sb.append(statement.getSqltext()); sb.append("</td></tt>"); // params column sb.append("<td>"); List<StmtParameter> params = CatalogUtil.getSortedCatalogItems(statement.getParameters(), "index"); List<String> paramTypes = new ArrayList<String>(); for (StmtParameter param : params) { paramTypes.add(VoltType.get((byte) param.getJavatype()).name()); } if (paramTypes.size() == 0) { sb.append("<i>None</i>"); } sb.append(StringUtils.join(paramTypes, ", ")); sb.append("</td>"); // r/w column sb.append("<td>"); if (statement.getReadonly()) { tag(sb, "success", "Read"); } else { tag(sb, "warning", "Write"); } sb.append("</td>"); // attributes sb.append("<td>"); if (!statement.getIscontentdeterministic() || !statement.getIsorderdeterministic()) { tag(sb, "inverse", "Determinism"); } if (statement.getSeqscancount() > 0) { tag(sb, "important", "Scans"); } sb.append("</td>"); sb.append("</tr>\n"); // BUILD THE DROPDOWN FOR THE PLAN/DETAIL TABLE sb.append( "<tr class='dropdown2'><td colspan='5' id='p-" + procedure.getTypeName().toLowerCase() + "-" + statement.getTypeName().toLowerCase() + "--dropdown'>\n"); sb.append("<div class='well well-small'><h4>Explain Plan:</h4>\n"); StatementAnnotation annotation = (StatementAnnotation) statement.getAnnotation(); if (annotation != null) { String plan = annotation.explainPlan; plan = plan.replace("\n", "<br/>"); plan = plan.replace(" ", " "); for (Table t : annotation.tablesRead) { String name = t.getTypeName().toUpperCase(); String link = "\"<a href='#s-" + t.getTypeName() + "'>" + name + "</a>\""; plan = plan.replace("\"" + name + "\"", link); } for (Table t : annotation.tablesUpdated) { String name = t.getTypeName().toUpperCase(); String link = "\"<a href='#s-" + t.getTypeName() + "'>" + name + "</a>\""; plan = plan.replace("\"" + name + "\"", link); } for (Index i : annotation.indexesUsed) { Table t = (Table) i.getParent(); String name = i.getTypeName().toUpperCase(); String link = "\"<a href='#s-" + t.getTypeName() + "-" + i.getTypeName() + "'>" + name + "</a>\""; plan = plan.replace("\"" + name + "\"", link); } sb.append("<tt>").append(plan).append("</tt>"); } else { sb.append("<i>No SQL explain plan found.</i>\n"); } sb.append("</div>\n"); sb.append("</td></tr>\n"); return sb.toString(); }