std::string DbSqlEditorContextHelp::helpTopicFromPosition()

in backend/wbprivate/sqlide/wb_sql_editor_help.cpp [700:1204]


std::string DbSqlEditorContextHelp::helpTopicFromPosition(HelpContext *helpContext, const std::string &query,
                                                          size_t caret) {
  logDebug2("Finding help topic\n");

  // We are not interested in validity here. We simply parse in default mode (LL) and examine the returned parse tree.
  // This usually will give us a good result, except in cases where the query has an error before the caret such that
  // we cannot predict the path through the rules.
  ParserRuleContext *parseTree = helpContext->_d->parse(query);
  //std::cout << "Parse tree: " << parseTree->toStringTree(&helpContext->_d->parser) << std::endl;

  tree::ParseTree *tree = MySQLRecognizerCommon::contextFromPosition(parseTree, caret);

  if (tree == nullptr)
    return "";

  if (antlrcpp::is<tree::TerminalNode *>(tree)) {
    tree::TerminalNode *node = dynamic_cast<tree::TerminalNode *>(tree);
    size_t token = node->getSymbol()->getType();
    if (token == MySQLLexer::SEMICOLON_SYMBOL) {
      tree = MySQLRecognizerCommon::getPrevious(tree);
      node = dynamic_cast<tree::TerminalNode *>(tree);
      token = node->getSymbol()->getType();
    }

    // First check if we can get a topic for this single token, either from our topic table or by lookup.
    // This is a double-edged sword. It will help in incomplete statements where we do not get a good parse tree
    // but might also show help topics for unrelated stuff (e.g. returns "SAVEPOINT" for "select a := savepoint(c);").
    if (supportedOperatorsAndKeywords.count(token) > 0)
      return supportedOperatorsAndKeywords[token];

    switch (token) {
      case MySQLLexer::MINUS_OPERATOR:
        if (isParentContext(tree, MySQLParser::RuleSimpleExpr))
          return "- UNARY";
        return "- BINARY";

      case MySQLLexer::BINARY_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleDataType))
          return "BINARY";
        if (isParentContext(tree, MySQLParser::RuleSimpleExpr) || isParentContext(tree, MySQLParser::RuleCastType))
          return "BINARY OPERATOR";

        tree = tree->parent;
        break;

      case MySQLLexer::CHANGE_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleChangeMaster))
          return "CHANGE MASTER TO";
        if (isParentContext(tree, MySQLParser::RuleChangeReplication))
          return "CHANGE REPLICATION FILTER";

        tree = tree->parent;
        break;

      // Other keywords connected to topics.
      case MySQLLexer::BEGIN_SYMBOL:
      case MySQLLexer::END_SYMBOL:
        return "BEGIN END";

      case MySQLLexer::TRUE_SYMBOL:
      case MySQLLexer::FALSE_SYMBOL:
        return "TRUE FALSE";

      case MySQLLexer::BINLOG_SYMBOL:
        if (isParentContext(tree, MySQLParser::RuleOtherAdministrativeStatement))
          return "BINLOG";
        if (isParentContext(tree, MySQLParser::RuleShowStatement))
          return "SHOW BINLOG EVENTS";

        tree = tree->parent;
        break;

      default:
        std::string s = base::toupper(node->getText());
        if (specialWords.count(s) == 0 && topicExists(helpContext->serverVersion(), s))
          return s;

        // No specific help topic for the given terminal. Jump to the token's parent and start the
        // context search then.
        tree = tree->parent;
        break;
    }
  }

  // See if we have a help topic for the given tree. If not walk up the parent chain until we find something.
  while (true) {
    if (tree == nullptr)
      return "";

    // We deliberately don't check if the given tree is actually a parse rule context - there is no other possibility.
    ParserRuleContext *context = dynamic_cast<ParserRuleContext *>(tree);
    size_t ruleIndex = context->getRuleIndex();
    if (contextToTopic.count(ruleIndex) > 0)
      return contextToTopic[ruleIndex];

    // Topics from function names
    std::string functionTopic = functionTopicForContext(context);
    if (!functionTopic.empty() && topicExists(helpContext->serverVersion(), functionTopic))
      return functionTopic;

    switch (ruleIndex) {
      case MySQLParser::RulePredicateOperations: {
        if (!context->children.empty()) {
          // Some keyword topics have variants with a leading NOT.
          auto parent = dynamic_cast<MySQLParser::PredicateContext *>(context->parent);
          bool isNot = parent->notRule() != nullptr;

          // IN, BETWEEN (with special help topic name), LIKE, REGEXP
          auto predicateContext = dynamic_cast<MySQLParser::PredicateOperationsContext *>(context);
          if (isToken(predicateContext->children[0], MySQLLexer::BETWEEN_SYMBOL)) {
            if (isNot)
              return "NOT BETWEEN";
            return "BETWEEN AND";
          }
          std::string topic = isNot ? "NOT " : "";
          return topic + base::toupper(predicateContext->children[0]->getText());
        }
        break;
      }

      case MySQLParser::RuleOtherAdministrativeStatement: {
        // See if we only have a single flush command.
        auto adminContext = dynamic_cast<MySQLParser::OtherAdministrativeStatementContext *>(context);
        if (adminContext->type->getType() == MySQLLexer::FLUSH_SYMBOL && adminContext->flushOption().size() == 1 &&
            adminContext->flushOption(0)->option->getType() == MySQLLexer::QUERY_SYMBOL)
          return "FLUSH QUERY CACHE";

        if (adminContext->type != nullptr)
          return tokenToTopic[adminContext->type->getType()];
        break;
      }

      case MySQLParser::RuleInsertStatement: {
        auto insertContext = dynamic_cast<MySQLParser::InsertStatementContext *>(context);
        if (insertContext->insertQueryExpression() != nullptr)
          return "INSERT SELECT";
        if (insertContext->insertLockOption() != nullptr &&
            insertContext->insertLockOption()->DELAYED_SYMBOL() != nullptr)
          return "INSERT DELAYED";
        return "INSERT";
      }

      case MySQLParser::RuleInstallUninstallStatment: {
        auto pluginContext = dynamic_cast<MySQLParser::InstallUninstallStatmentContext *>(context);
        if (pluginContext->action != nullptr)
          return tokenToTopic[pluginContext->action->getType()];
        break;
      }

      case MySQLParser::RuleExpr: {
        auto exprContext = dynamic_cast<MySQLParser::ExprContext *>(context);
        if (exprContext->children.size() > 2 && isToken(exprContext->children[1], MySQLLexer::IS_SYMBOL)) {
          if (isToken(exprContext->children[2], MySQLLexer::NOT_SYMBOL) ||
              isToken(exprContext->children[2], MySQLLexer::NOT2_SYMBOL))
            return "IS NOT";
          return "IS";
        }
        break;
      }

      case MySQLParser::RuleBoolPri: {
        if (antlrcpp::is<MySQLParser::PrimaryExprIsNullContext *>(context)) {
          auto primaryExprIsNullContext = dynamic_cast<MySQLParser::PrimaryExprIsNullContext *>(context);
          if (primaryExprIsNullContext->notRule() == nullptr)
            return "IS NULL";
          return "IS NOT NULL";
        }
        break;
      }

      case MySQLParser::RuleSetStatement: {
        auto setContext = dynamic_cast<MySQLParser::SetStatementContext *>(context);
        auto setValueListContext = setContext->startOptionValueList();
        if (setValueListContext->TRANSACTION_SYMBOL() != nullptr)
          return "ISOLATION";

        if (setValueListContext->PASSWORD_SYMBOL().size() > 0)
          return "SET PASSWORD";

        // TODO: handle the assignment at the caret instead of looking for specific names in the first assignment.
        ParserRuleContext *variableName = nullptr;
        if (setValueListContext->startOptionValueListFollowingOptionType() != nullptr) {
          auto subContext = setValueListContext->startOptionValueListFollowingOptionType();
          variableName = subContext->optionValueFollowingOptionType()->internalVariableName();
        } else if (setValueListContext->optionValueNoOptionType() != nullptr)
          variableName = setValueListContext->optionValueNoOptionType()->internalVariableName();
        if (variableName != nullptr) {
          std::string option = base::toupper(variableName->getText());
          if (option == "SQL_SLAVE_SKIP_COUNTER")
            return "SET GLOBAL SQL_SLAVE_SKIP_COUNTER";
          if (option == "SQL_LOG_BIN")
            return "SET SQL_LOG_BIN";
        }
        return "SET";
      }

      case MySQLParser::RuleLoadStatement: {
        auto loadStatementContext = dynamic_cast<MySQLParser::LoadStatementContext *>(context);
        if (loadStatementContext->dataOrXml()->DATA_SYMBOL() != nullptr)
          return "LOAD DATA";
        return "LOAD XML";
      }

      case MySQLParser::RulePredicate:
        if (context->children.size() > 2) {
          if (isToken(context->children[1], MySQLLexer::NOT_SYMBOL) ||
              isToken(context->children[1], MySQLLexer::NOT2_SYMBOL)) {
            // For NOT BETWEEN, NOT LIKE, NOT IN, NOT REGEXP.
            auto predicateContext = dynamic_cast<MySQLParser::PredicateOperationsContext *>(context->children[2]);
            return "NOT " + base::toupper(predicateContext->children[0]->getText());
          }
          if (isToken(context->children[1], MySQLLexer::SOUNDS_SYMBOL))
            return "SOUNDS LIKE";
        }
        break;

      case MySQLParser::RuleTableAdministrationStatement: {
        auto adminStatementContext = dynamic_cast<MySQLParser::TableAdministrationStatementContext *>(context);
        if (adminStatementContext->type != nullptr)
          return tokenToTopic[adminStatementContext->type->getType()];
        break;
      }

      case MySQLParser::RulePreparedStatement: {
        auto preparedContext = dynamic_cast<MySQLParser::PreparedStatementContext *>(context);
        size_t type = 0;
        if (preparedContext->type != nullptr)
          type = preparedContext->type->getType();
        if (type == MySQLLexer::PREPARE_SYMBOL)
          return "PREPARE";
        if (type == MySQLLexer::DEALLOCATE_SYMBOL || type == MySQLLexer::DROP_SYMBOL)
          return "DEALLOCATE PREPARE";
        break;
      }

      case MySQLParser::RuleReplicationStatement: {
        auto replicationContext = dynamic_cast<MySQLParser::ReplicationStatementContext *>(context);
        if (replicationContext->PURGE_SYMBOL() != nullptr)
          return "PURGE BINARY LOGS";
        if (replicationContext->RESET_SYMBOL() != nullptr &&
            (replicationContext->resetOption().empty() || replicationContext->resetOption()[0]->option == nullptr))
          return "RESET";
        break;
      }

      case MySQLParser::RuleResetOption: {
        auto optionContext = dynamic_cast<MySQLParser::ResetOptionContext *>(context);
        if (isToken(optionContext->option, MySQLLexer::MASTER_SYMBOL))
          return "RESET MASTER";
        if (isToken(optionContext->option, MySQLLexer::SLAVE_SYMBOL))
          return "RESET SLAVE";
        return "RESET";
      }

      case MySQLParser::RuleShowStatement: {
        auto showContext = dynamic_cast<MySQLParser::ShowStatementContext *>(context);
        if (showContext->value == nullptr) {
          if (showContext->charset() != nullptr)
            return "SHOW CHARACTER SET";
          return "SHOW";
        }

        switch (showContext->value->getType()) {
          case MySQLLexer::TABLE_SYMBOL:
            return "SHOW TABLE STATUS";
          case MySQLLexer::SLAVE_SYMBOL:
            if (showContext->HOSTS_SYMBOL() != nullptr)
              return "SHOW SLAVE HOSTS";
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW SLAVE STATUS";
            break;
          case MySQLLexer::CREATE_SYMBOL: {
            if (showContext->object != nullptr)
              return "SHOW CREATE " + base::toupper(showContext->object->getText());
            break;
          }
          case MySQLLexer::PROCEDURE_SYMBOL:
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW PROCEDURE STATUS";
            if (showContext->CODE_SYMBOL() != nullptr)
              return "SHOW PROCEDURE CODE";
            break;
          case MySQLLexer::FUNCTION_SYMBOL:
            if (showContext->STATUS_SYMBOL() != nullptr)
              return "SHOW FUNCTION STATUS";
            if (showContext->CODE_SYMBOL() != nullptr)
              return "SHOW FUNCTION CODE";
            break;

          default:
            return tokenToTopic[showContext->value->getType()];
        }

        break;
      }

      case MySQLParser::RuleTableConstraintDef: {
        auto definitionContext = dynamic_cast<MySQLParser::TableConstraintDefContext *>(context);
        if (definitionContext->type != nullptr && definitionContext->type->getType() == MySQLLexer::FOREIGN_SYMBOL)
          return "CONSTRAINT";
        if (definitionContext->checkConstraint() != nullptr)
          return "CONSTRAINT"; // TODO: There's no own topic for check constraints so far. Update when that is available.
        break;
      }

      case MySQLParser::RuleHelpCommand:
        return "HELP COMMAND";

      case MySQLParser::RuleSimpleExpr:
        if (!context->children.empty() && antlrcpp::is<tree::TerminalNode *>(context->children[0])) {
          size_t type = dynamic_cast<tree::TerminalNode *>(context->children[0])->getSymbol()->getType();
          switch (type) {
            case MySQLLexer::MATCH_SYMBOL:
              return "MATCH AGAINST";
            case MySQLLexer::CONVERT_SYMBOL:
              return "CONVERT";
            case MySQLLexer::DEFAULT_SYMBOL:
              return "DEFAULT";
            case MySQLLexer::CASE_SYMBOL:
              return "CASE OPERATOR";
          }
        }
        break;

      case MySQLParser::RuleEngineRef: {
        std::string engine = base::tolower(context->getText());
        if (engine == "merge" || engine == "mrg_myisam")
          return "MERGE";
        break;
      }

      case MySQLParser::RuleSlave:
        if (!context->children.empty())
          return base::toupper(context->children[0]->getText()) + " SLAVE";
        break;

      case MySQLParser::RuleDataType: {
        auto typeContext = dynamic_cast<MySQLParser::DataTypeContext *>(context);
        if (typeContext->nchar() != nullptr)
          return "CHAR";

        std::string topic;
        switch (typeContext->type->getType()) {
          case MySQLLexer::DOUBLE_SYMBOL:
            if (typeContext->PRECISION_SYMBOL() != nullptr)
              return "DOUBLE PRECISION";
            return "DOUBLE";

          case MySQLLexer::SET_SYMBOL:
            return "SET DATA TYPE";

          case MySQLLexer::YEAR_SYMBOL:
            return "YEAR DATA TYPE";

          case MySQLLexer::BOOL_SYMBOL:
            return "BOOLEAN";

          case MySQLLexer::CHAR_SYMBOL:
          case MySQLLexer::NCHAR_SYMBOL:
          case MySQLLexer::NATIONAL_SYMBOL:
          case MySQLLexer::VARCHAR_SYMBOL:
          case MySQLLexer::NVARCHAR_SYMBOL:
          case MySQLLexer::VARYING_SYMBOL:
            if (typeContext->VARYING_SYMBOL() != nullptr || typeContext->VARCHAR_SYMBOL() != nullptr)
              return "VARCHAR";
            if (typeContext->charsetWithOptBinary() != nullptr &&
                typeContext->charsetWithOptBinary()->BYTE_SYMBOL() != nullptr)
              return "CHAR BYTE";
            return "CHAR";

          default:
            topic = base::toupper(typeContext->type->getText());
            break;
        }

        if (topicExists(helpContext->serverVersion(), topic))
          return topic;

        break; // Not all data types have an own topic.
      }

      case MySQLParser::RuleFromClause: {
        auto keylistContext = dynamic_cast<MySQLParser::FromClauseContext *>(context);
        if (keylistContext->DUAL_SYMBOL() != nullptr)
          return "DUAL";
        break;
      }

      case MySQLParser::RuleTransactionCharacteristics: {
        auto characteristicsContext = dynamic_cast<MySQLParser::TransactionCharacteristicsContext *>(context);
        if (characteristicsContext->isolationLevel() != nullptr)
          return "ISOLATION";
        break;
      }

      case MySQLParser::RuleSubstringFunction: {
        // A case where we might have a synonym, so we need to check the text actually.
        auto substringContext = dynamic_cast<MySQLParser::SubstringFunctionContext *>(context);
        return base::toupper(substringContext->SUBSTRING_SYMBOL()->getText());

        break;
      }

      case MySQLParser::RuleAlterStatement: {
        auto alterContext = dynamic_cast<MySQLParser::AlterStatementContext *>(context);
        if (alterContext->alterTable() != nullptr)
          return "ALTER TABLE";
        if (alterContext->alterDatabase() != nullptr)
          return "ALTER DATABASE";
        if (alterContext->PROCEDURE_SYMBOL() != nullptr)
          return "ALTER PROCEDURE";
        if (alterContext->FUNCTION_SYMBOL() != nullptr)
          return "ALTER FUNCTION";
        if (alterContext->alterView() != nullptr)
          return "ALTER VIEW";
        if (alterContext->alterEvent() != nullptr)
          return "ALTER EVENT";
        if (alterContext->alterTablespace() != nullptr || alterContext->alterUndoTablespace() != nullptr)
          return "ALTER TABLESPACE";
        if (alterContext->alterLogfileGroup() != nullptr)
          return "ALTER LOGFILE GROUP";
        if (alterContext->alterServer() != nullptr)
          return "ALTER SERVER";
        if (alterContext->INSTANCE_SYMBOL() != nullptr)
          return "ALTER INSTANCE";

        break;
      }

      case MySQLParser::RuleCreateStatement: {
        auto createContext = dynamic_cast<MySQLParser::CreateStatementContext *>(context);
        if (createContext->createDatabase() != nullptr)
          return "CREATE DATABASE";
        if (createContext->createTable() != nullptr)
          return "CREATE TABLE";
        if (createContext->createFunction() != nullptr)
          return "CREATE FUNCTION";
        if (createContext->createProcedure() != nullptr)
          return "CREATE PROCEDURE";
        if (createContext->createUdf() != nullptr)
          return "CREATE FUNCTION UDF";
        if (createContext->createLogfileGroup() != nullptr)
          return "CREATE LOGFILE GROUP";
        if (createContext->createView() != nullptr)
          return "CREATE VIEW";
        if (createContext->createTrigger() != nullptr)
          return "CREATE TRIGGER";
        if (createContext->createIndex() != nullptr)
          return "CREATE INDEX";
        if (createContext->createServer() != nullptr)
          return "CREATE SERVER";
        if (createContext->createTablespace() != nullptr)
          return "CREATE TABLESPACE";
        if (createContext->createEvent() != nullptr)
          return "CREATE EVENT";
        if (createContext->createRole() != nullptr)
          return "CREATE ROLE";
        if (createContext->createSpatialReference() != nullptr)
          return "CREATE SPATIALREFERENCE SYSTEM";
        if (createContext->createUndoTablespace() != nullptr)
          return "CREATE TABLESPACE";

        break;
      }

      case MySQLParser::RuleDropStatement: {
        auto dropContext = dynamic_cast<MySQLParser::DropStatementContext *>(context);
        if (dropContext->dropDatabase() != nullptr)
          return "DROP DATABASE";
        if (dropContext->dropEvent() != nullptr)
          return "DROP EVENT";
        if (dropContext->dropFunction() != nullptr) // We cannot make a distinction between FUNCTION and FUNCTION UDF.
          return "DROP FUNCTION";
        if (dropContext->dropProcedure() != nullptr)
          return "DROP PROCEDURE";
        if (dropContext->dropIndex() != nullptr)
          return "DROP INDEX";
        if (dropContext->dropLogfileGroup() != nullptr)
          return "DROP LOGFILE GROUP";
        if (dropContext->dropServer() != nullptr)
          return "DROP SERVER";
        if (dropContext->dropTable() != nullptr)
          return "DROP TABLE";
        if (dropContext->dropTableSpace() != nullptr)
          return "DROP TABLESPACE";
        if (dropContext->dropTrigger() != nullptr)
          return "DROP TRIGGER";
        if (dropContext->dropView() != nullptr)
          return "DROP VIEW";
        if (dropContext->dropRole() != nullptr)
          return "DROP ROLE";
        if (dropContext->dropSpatialReference() != nullptr)
          return "DROP SPATIALREFERENCE SYSTEM";
        if (dropContext->dropUndoTablespace() != nullptr)
          return "DROP TABLESPACE";

        break;
      }
    }

    tree = tree->parent;
  }

  return "";
}