parsers::Sql_completion_result getCodeCompletion()

in mysqlshdk/libs/parser/code-completion/mysql-code-completion.cpp [1219:1796]


parsers::Sql_completion_result getCodeCompletion(size_t offset,
                                                 std::string_view active_schema,
                                                 bool uppercaseKeywords,
                                                 bool filtered,
                                                 MySQLParser *parser) {
  log_debug("Invoking code completion");

  static const std::map<size_t, std::vector<std::string>> synonyms = {
      {MySQLLexer::CHAR_SYMBOL, {"CHARACTER"}},
      {MySQLLexer::NOW_SYMBOL,
       {"CURRENT_TIMESTAMP", "LOCALTIME", "LOCALTIMESTAMP"}},
      {MySQLLexer::DAY_SYMBOL, {"DAYOFMONTH"}},
      {MySQLLexer::DECIMAL_SYMBOL, {"DEC"}},
      {MySQLLexer::DISTINCT_SYMBOL, {"DISTINCTROW"}},
      {MySQLLexer::CHAR_SYMBOL, {"CHARACTER"}},
      {MySQLLexer::COLUMNS_SYMBOL, {"FIELDS"}},
      {MySQLLexer::FLOAT_SYMBOL, {"FLOAT4"}},
      {MySQLLexer::DOUBLE_SYMBOL, {"FLOAT8"}},
      {MySQLLexer::INT_SYMBOL, {"INTEGER", "INT4"}},
      {MySQLLexer::RELAY_THREAD_SYMBOL, {"IO_THREAD"}},
      {MySQLLexer::SUBSTRING_SYMBOL, {"MID"}},
      {MySQLLexer::MID_SYMBOL, {"MEDIUMINT"}},
      {MySQLLexer::MEDIUMINT_SYMBOL, {"MIDDLEINT"}},
      {MySQLLexer::NDBCLUSTER_SYMBOL, {"NDB"}},
      {MySQLLexer::REGEXP_SYMBOL, {"RLIKE"}},
      {MySQLLexer::DATABASE_SYMBOL, {"SCHEMA"}},
      {MySQLLexer::DATABASES_SYMBOL, {"SCHEMAS"}},
      {MySQLLexer::USER_SYMBOL, {"SESSION_USER"}},
      {MySQLLexer::STD_SYMBOL, {"STDDEV", "STDDEV_POP"}},
      {MySQLLexer::SUBSTRING_SYMBOL, {"SUBSTR"}},
      {MySQLLexer::VARCHAR_SYMBOL, {"VARCHARACTER"}},
      {MySQLLexer::VARIANCE_SYMBOL, {"VAR_POP"}},
      {MySQLLexer::TINYINT_SYMBOL, {"INT1"}},
      {MySQLLexer::SMALLINT_SYMBOL, {"INT2"}},
      {MySQLLexer::MEDIUMINT_SYMBOL, {"INT3"}},
      {MySQLLexer::BIGINT_SYMBOL, {"INT8"}},
      {MySQLLexer::SECOND_SYMBOL, {"SQL_TSI_SECOND"}},
      {MySQLLexer::MINUTE_SYMBOL, {"SQL_TSI_MINUTE"}},
      {MySQLLexer::HOUR_SYMBOL, {"SQL_TSI_HOUR"}},
      {MySQLLexer::DAY_SYMBOL, {"SQL_TSI_DAY"}},
      {MySQLLexer::WEEK_SYMBOL, {"SQL_TSI_WEEK"}},
      {MySQLLexer::MONTH_SYMBOL, {"SQL_TSI_MONTH"}},
      {MySQLLexer::QUARTER_SYMBOL, {"SQL_TSI_QUARTER"}},
      {MySQLLexer::YEAR_SYMBOL, {"SQL_TSI_YEAR"}},
  };

  // Also create a separate scanner which allows us to easily navigate the
  // tokens without affecting the token stream used by the parser.
  Scanner scanner(
      dynamic_cast<BufferedTokenStream *>(parser->getTokenStream()));

  // Move to the token that ends past the given offset. Note that we don't check
  // start of the token here, because if a word which does not form a token yet
  // (i.e. an identifier which starts with a back tick) is being completed, it's
  // not going to be on the tokens list and we want to move past it.
  do {
    log_debug3(
        "|%s| - %s (%zu), %zu-%zu", scanner.tokenText().c_str(),
        parser->getVocabulary().getDisplayName(scanner.tokenType()).c_str(),
        scanner.tokenType(), scanner.tokenOffset(), scanner.tokenEndOffset());

    if (offset <= scanner.tokenEndOffset()) {
      break;
    }
  } while (scanner.next(false));

  // Store position on the scanner stack.
  scanner.push();

  AutoCompletionContext context;
  context.collectCandidates(parser, scanner);

  MySQLQueryType queryType = QtUnknown;
  MySQLLexer *lexer =
      dynamic_cast<MySQLLexer *>(parser->getTokenStream()->getTokenSource());
  if (lexer != nullptr) {
    lexer->reset();  // Set back the input position to the beginning for query
                     // type determination.
    queryType = lexer->determineQueryType();
  }

  parsers::Sql_completion_result r;

  r.context.prefix.full = get_prefix(parser, &scanner, offset);
  init_prefix_info(&r.context.prefix, is_ansi_quotes_active(*parser));

  {
    const auto &vocabulary = parser->getVocabulary();
    const auto process_entry = [](std::string *e) {
      if (e->rfind("_SYMBOL") != std::string::npos) {
        e->resize(e->size() - 7);
      } else {
        *e = unquote_string(*e);
      }
    };
    // use the full prefix here, if it begins with a quote we don't want to
    // to match any keywords
    const auto should_insert_entry =
        [filtered, &prefix = std::as_const(r.context.prefix.full)](
            const std::string &entry) {
          // entry is a keyword or a function, ASCII-based check is sufficient
          return !filtered || shcore::str_ibeginswith(entry, prefix);
        };

    bool function_call = false;

    const auto add_entry = [&should_insert_entry, &function_call, &r,
                            &uppercaseKeywords](std::string e,
                                                const char *msg) {
      if (should_insert_entry(e)) {
        if (!uppercaseKeywords) e = shcore::str_lower(e);

        if (function_call) {
          insert_system_function(&r, e);
        } else {
          insert(&r.keywords, std::move(e), msg);
        }
      }
    };

    // if we're at the beginning of a statement add the DELIMITER command
    if (scanner.tokenIndex() == 0 ||
        (scanner.tokenIndex() == 1 && scanner.tokenType() == MySQLLexer::EOF)) {
      add_entry("DELIMITER", "a command");
    }

    for (const auto &candidate : context.completionCandidates.tokens) {
      if (antlr4::Token::EOF == candidate.first ||
          antlr4::Token::EPSILON == candidate.first) {
        continue;
      }

      auto entry = vocabulary.getDisplayName(candidate.first);
      function_call = false;

      process_entry(&entry);

      if (!candidate.second.empty()) {
        // A function call?
        if (candidate.second[0] == MySQLLexer::OPEN_PAR_SYMBOL) {
          function_call = true;
        } else {
          for (auto token : candidate.second) {
            auto subentry = vocabulary.getDisplayName(token);

            process_entry(&subentry);

            if (!MySQLLexer::isOperator(token)) {
              entry += " ";
            }

            entry += subentry;
          }
        }
      }

      add_entry(std::move(entry), "a keyword");

      // Add also synonyms, if there are any.
      if (const auto s = synonyms.find(candidate.first); s != synonyms.end()) {
        for (const auto &synonym : s->second) {
          add_entry(synonym, "a synonym");
        }
      }
    }
  }

  for (const auto &candidate : context.completionCandidates.rules) {
    // Restore the scanner position to the caret position and store that value
    // again for the next round.
    scanner.pop();
    scanner.push();

    if (shcore::Logger::LOG_LEVEL::LOG_DEBUG3 ==
        shcore::current_logger()->get_log_level()) {
      const auto &rules = parser->getRuleNames();
      log_debug3("Rule: '%s' (%zu)", rules[candidate.second.front()].c_str(),
                 candidate.second.front());

      auto it = candidate.second.begin();
      while (++it != candidate.second.end()) {
        log_debug3("      '%s' (%zu)", rules[*it].c_str(), *it);
      }

      log_debug3("      '%s' (%zu)", rules[candidate.first].c_str(),
                 candidate.first);
    }

    switch (candidate.first) {
      case MySQLParser::RuleRuntimeFunctionCall: {
        log_debug3("Adding runtime function names");
        r.candidates.emplace(Candidate::RUNTIME_FUNCTION);
        break;
      }

      case MySQLParser::RuleFunctionRef:
      case MySQLParser::RuleFunctionCall: {
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if (qualifier.empty()) {
          log_debug3("Adding user defined function names");
          r.candidates.emplace(Candidate::UDF);
        }

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) qualifier = active_schema;

          insert_functions(&r, qualifier);
        }

        break;
      }

      case MySQLParser::RuleEngineRef: {
        log_debug3("Adding engine names");
        r.candidates.emplace(Candidate::ENGINE);
        break;
      }

      case MySQLParser::RuleSchemaRef: {
        insert_schemas(&r);
        break;
      }

      case MySQLParser::RuleProcedureRef: {
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) qualifier = active_schema;

          insert_procedures(&r, qualifier);
        }
        break;
      }

      case MySQLParser::RuleTableRefWithWildcard: {
        // A special form of table references (id.id.*) used only in multi-table
        // delete. Handling is similar as for column references (just that we
        // have table/view objects instead of column refs).
        std::string_view schema, table;
        ObjectFlags flags =
            determineSchemaTableQualifier(scanner, lexer, &r, &schema, &table);

        if ((flags & ShowSchemas) != 0) insert_schemas(&r);

        if ((flags & ShowTables) != 0) {
          if (schema.empty()) {
            schema = active_schema;
          }

          insert_tables(&r, schema);
          insert_views(&r, schema);
        }
        break;
      }

      case MySQLParser::RuleTableRef:
      case MySQLParser::RuleFilterTableRef: {
        // Tables refs - also allow view refs (conditionally).
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) {
            qualifier = active_schema;
          }

          insert_tables(&r, qualifier);

          if (QtSelect == queryType) {
            insert_views(&r, qualifier);
          }
        }
        break;
      }

      case MySQLParser::RuleTableWild:
      case MySQLParser::RuleColumnRef: {
        // Try limiting what to show to the smallest set possible.
        // If we have table references show columns only from them.
        // Show columns from the default schema only if there are no _
        std::string_view schema, table;
        ObjectFlags flags =
            determineSchemaTableQualifier(scanner, lexer, &r, &schema, &table);

        if ((flags & ShowSchemas) != 0) insert_schemas(&r);

        // If a schema is given then list only tables + columns from that
        // schema. If no schema is given but we have table references use the
        // schemas from them. Otherwise use the default schema.
        // TODO: case sensitivity.
        Object_names schemas;

        if (!schema.empty()) {
          schemas.insert(schema);
        } else if (!context.references.empty()) {
          for (const auto &reference : context.references) {
            if (!reference.schema.empty()) schemas.insert(reference.schema);
          }
        }

        if (schemas.empty()) schemas.insert(active_schema);

        if ((flags & ShowTables) != 0) {
          insert_tables(&r, schemas);

          if (candidate.first == MySQLParser::RuleColumnRef) {
            // Insert also views.
            insert_views(&r, schemas);

            // Insert also tables from our references list.
            for (const auto &reference : context.references) {
              // If no schema was specified then allow also tables without a
              // given schema. Otherwise the reference's schema must match any
              // of the specified schemas (which include those from the ref
              // list).
              if ((schema.empty() && reference.schema.empty()) ||
                  (schemas.count(reference.schema) > 0)) {
                const auto &t =
                    reference.alias.empty() ? reference.table : reference.alias;

                if (!filtered ||
                    ibegins_with(t, r.context.prefix.as_identifier)) {
                  insert_table(&r, t);
                }
              }
            }
          }
        }

        if ((flags & ShowColumns) != 0) {
          const auto add_referenced_table =
              [&active_schema, &r](const Table_reference &reference) {
                const auto s =
                    reference.schema.empty() ? active_schema : reference.schema;

                if (!s.empty()) {
                  insert_columns(&r, s, reference.table);
                }
              };

          switch (r.context.qualifier.size()) {
            case 0:
              // no qualifiers, get columns from referenced tables
              for (const auto &reference : context.references) {
                add_referenced_table(reference);
              }
              break;

            case 1:
              // if there's a default schema, get columns from the given table
              // in that schema
              if (!active_schema.empty()) {
                insert_columns(&r, active_schema, table);
              }

              // this could be an alias
              for (const auto &reference : context.references) {
                if (table == reference.alias) {
                  add_referenced_table(reference);
                  break;
                }
              }

              // Special deal here: triggers. Show columns for the "new" and
              // "old" qualifiers too. Use the first reference in the list,
              // which is the table to which this trigger belongs (there can be
              // more if the trigger body references other tables).
              if (queryType == QtCreateTrigger && !context.references.empty() &&
                  (shcore::str_caseeq(table, "old") ||
                   shcore::str_caseeq(table, "new"))) {
                add_referenced_table(context.references[0]);
              }
              break;

            case 2:
              // columns from schema.table
              insert_columns(&r, schema, table);
              break;
          }
        }

        break;
      }

      case MySQLParser::RuleColumnInternalRef: {
        if (!context.references.empty()) {
          const auto &reference = context.references[0];
          const auto s =
              reference.schema.empty() ? active_schema : reference.schema;

          if (!s.empty()) {
            insert_internal_columns(&r, s, reference.table);
          }
        }
        break;
      }

      case MySQLParser::RuleTriggerRef: {
        // While triggers are bound to a table they are schema objects and are
        // referenced as "[schema.]trigger" e.g. in DROP TRIGGER.
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) {
            qualifier = active_schema;
          }

          insert_triggers(&r, qualifier);
        }
        break;
      }

      case MySQLParser::RuleViewRef: {
        // View refs only (no table references), e.g. like in DROP VIEW ...
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) {
            qualifier = active_schema;
          }

          insert_views(&r, qualifier);
        }
        break;
      }

      case MySQLParser::RuleLogfileGroupRef: {
        log_debug3("Adding logfile group names");
        r.candidates.emplace(Candidate::LOGFILE_GROUP);
        break;
      }

      case MySQLParser::RuleTablespaceRef: {
        log_debug3("Adding tablespace names");
        r.candidates.emplace(Candidate::TABLESPACE);
        break;
      }

      case MySQLParser::RuleUserVariableIdentifier: {
        log_debug3("Adding user variables");
        r.candidates.emplace(Candidate::USER_VAR);
        break;
      }

      case MySQLParser::RuleLabelRef: {
        log_debug3("Adding label references");
        r.candidates.emplace(Candidate::LABEL);
        break;
      }

      case MySQLParser::RuleLvalueVariable:
      case MySQLParser::RuleRvalueSystemVariable: {
        insert_system_variables(&r);
        break;
      }

      case MySQLParser::RuleCharsetName: {
        log_debug3("Adding charsets");
        r.candidates.emplace(Candidate::CHARSET);
        break;
      }

      case MySQLParser::RuleCollationName: {
        log_debug3("Adding collations");
        r.candidates.emplace(Candidate::COLLATION);
        break;
      }

      case MySQLParser::RuleEventRef: {
        std::string_view qualifier;
        ObjectFlags flags = determineQualifier(scanner, lexer, &r, &qualifier);

        if ((flags & ShowFirst) != 0) insert_schemas(&r);

        if ((flags & ShowSecond) != 0) {
          if (qualifier.empty()) {
            qualifier = active_schema;
          }

          insert_events(&r, qualifier);
        }
        break;
      }

      case MySQLParser::RuleUser: {
        log_debug3("Adding users");
        r.candidates.emplace(Candidate::USER);
        init_prefix_as_user(&scanner, &r.context);
        break;
      }

      case MySQLParser::RulePluginRef: {
        log_debug3("Adding plugins");
        r.candidates.emplace(Candidate::PLUGIN);
        break;
      }

      case MySQLParser::RuleIdentifier: {
        switch (queryType) {
          case QtShowColumns:
          case QtShowEvents:
          case QtShowIndexes:
          case QtShowOpenTables:
          case QtShowTableStatus:
          case QtShowTables:
          case QtShowTriggers:
          case QtUse: {
            std::string_view qualifier;
            const auto flags =
                determineQualifier(scanner, lexer, &r, &qualifier);

            // identifier in these queries can only by a schema, if there's
            // something like schema.prefix, it's an error
            if ((flags & ShowFirst) != 0) insert_schemas(&r);

            break;
          }

          case QtResetPersist:
            insert_system_variables(&r);
            break;

          default:
            break;
        }
        break;
      }

      case MySQLParser::RuleProcedureName:
      case MySQLParser::RuleFunctionName:
      case MySQLParser::RuleTriggerName:
      case MySQLParser::RuleViewName:
      case MySQLParser::RuleEventName:
      case MySQLParser::RuleTableName: {
        // these are used in: CREATE object object_name, object_name can be
        // qualified with schema, we can only suggest candidates if there's no
        // DOT
        std::string_view qualifier;
        const auto flags = determineQualifier(scanner, lexer, &r, &qualifier);
        if ((flags & ShowFirst) != 0) insert_schemas(&r);
        break;
      }
    }
  }

  scanner.pop();  // Clear the scanner stack.

  r.context.references = std::move(context.references);

  if (filtered) {
    r.context.labels.reserve(context.labels.size());

    for (auto &label : context.labels) {
      if (ibegins_with(label, r.context.prefix.as_identifier)) {
        r.context.labels.emplace_back(std::move(label));
      }
    }
  } else {
    r.context.labels = std::move(context.labels);
  }

  return r;
}