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;
}