gui/frontend/src/parsing/worker/RdbmsLanguageService.ts (431 lines of code) (raw):
/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms, as
* designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
import { IPosition, IRange, languages } from "monaco-editor";
import { ScopedSymbol, BaseSymbol, SymbolTable } from "antlr4-c3";
import { ICodeEditorModel } from "../../components/ui/CodeEditor/CodeEditor.js";
import { CompletionItem, CompletionList } from "../../components/ui/CodeEditor/index.js";
import { mapCompletionKind } from "../../script-execution/index.js";
import {
ICompletionData, ICompletionObjectDetails, ILanguageWorkerResultData, ILanguageWorkerSuggestionData,
ILanguageWorkerTaskData, LanguageCompletionKind, ServiceLanguage,
} from "../parser-common.js";
import {
CharsetSymbol, SystemVariableSymbol, SystemFunctionSymbol, CollationSymbol, ColumnSymbol, EngineSymbol, EventSymbol,
IndexSymbol, LabelSymbol, LogfileGroupSymbol, PluginSymbol, SchemaSymbol, StoredFunctionSymbol, ViewSymbol,
StoredProcedureSymbol, TablespaceSymbol, TableSymbol, TriggerSymbol, UdfSymbol, UserSymbol, UserVariableSymbol,
DBSymbolTable,
} from "../DBSymbolTable.js";
import { WorkerPool } from "../../supplement/WorkerPool.js";
import { Settings } from "../../supplement/Settings/Settings.js";
import { SQLExecutionContext } from "../../script-execution/SQLExecutionContext.js";
import { deepEqual } from "../../utilities/helpers.js";
// A string for lookup if schemas have been loaded already.
const schemaKey = "\u0010schemas\u0010";
// This class handles RDBMS specific language processing in the main (UI) thread.
export class RdbmsLanguageService {
// A symbol table which combines 3 sources:
// - All global symbols, which are available for a specific RDBMS type (library functions etc.).
// - Symbols for a specific connection (schemas, tables etc.).
// - Symbols from a local parse run (e.g. in stored routines).
protected readonly localSymbols: DBSymbolTable;
// TODO: get options from outer context (SQL page, settings, sort keys).
private readonly sortKeys = new Map<LanguageCompletionKind, string>([
[LanguageCompletionKind.Keyword, "01."],
[LanguageCompletionKind.Column, "02."],
[LanguageCompletionKind.UserVariable, "03."],
[LanguageCompletionKind.Label, "04."],
[LanguageCompletionKind.Table, "05."],
[LanguageCompletionKind.View, "06."],
[LanguageCompletionKind.Schema, "07."],
[LanguageCompletionKind.Function, "08."],
[LanguageCompletionKind.Procedure, "09."],
[LanguageCompletionKind.Udf, "10."],
[LanguageCompletionKind.Trigger, "11."],
[LanguageCompletionKind.Index, "12."],
[LanguageCompletionKind.Event, "13."],
[LanguageCompletionKind.User, "14."],
[LanguageCompletionKind.Engine, "15."],
[LanguageCompletionKind.Plugin, "16."],
[LanguageCompletionKind.LogfileGroup, "17."],
[LanguageCompletionKind.Tablespace, "18."],
[LanguageCompletionKind.Charset, "19."],
[LanguageCompletionKind.Collation, "20."],
[LanguageCompletionKind.SystemFunction, "21."],
[LanguageCompletionKind.SystemVariable, "22."],
]);
private readonly descriptionMap = new Map<LanguageCompletionKind, string>([
[LanguageCompletionKind.Keyword, "Keyword"],
[LanguageCompletionKind.Column, "Column"],
[LanguageCompletionKind.UserVariable, "User Variable"],
[LanguageCompletionKind.Label, "Label"],
[LanguageCompletionKind.Table, "Table"],
[LanguageCompletionKind.View, "View"],
[LanguageCompletionKind.Schema, "Schema"],
[LanguageCompletionKind.Function, "Stored Function"],
[LanguageCompletionKind.Procedure, "Stored Procedure"],
[LanguageCompletionKind.Udf, "User Defined Function"],
[LanguageCompletionKind.Trigger, "Trigger"],
[LanguageCompletionKind.Index, "Index"],
[LanguageCompletionKind.Event, "Event"],
[LanguageCompletionKind.User, "Uer"],
[LanguageCompletionKind.Engine, "Table Engine"],
[LanguageCompletionKind.Plugin, "Server Plugin"],
[LanguageCompletionKind.LogfileGroup, "Logfile Group"],
[LanguageCompletionKind.Tablespace, "Tablespace"],
[LanguageCompletionKind.Charset, "Charset"],
[LanguageCompletionKind.Collation, "Collation"],
[LanguageCompletionKind.SystemFunction, "System Function"],
[LanguageCompletionKind.SystemVariable, "System Variable"],
]);
private readonly loadedSchemaTables = new Set<string>();
public constructor(
private language: ServiceLanguage,
protected pool: WorkerPool<ILanguageWorkerTaskData, ILanguageWorkerResultData>,
private globalSymbols: SymbolTable) {
this.localSymbols = new DBSymbolTable("local", { allowDuplicateSymbols: false });
this.localSymbols.addDependencies(this.globalSymbols);
}
public async getCodeCompletionItems(context: SQLExecutionContext,
position: IPosition): Promise<CompletionList | undefined> {
const model = context.model as ICodeEditorModel;
if (model.symbols) {
this.localSymbols.addDependencies(model.symbols);
}
const statement = await context.getStatementAtPosition(position);
if (!statement) {
return;
}
return new Promise((resolve, reject) => {
const infoData: ILanguageWorkerSuggestionData = {
api: "suggestion",
language: this.language,
version: context.dbVersion,
sql: statement.text,
offset: model.getOffsetAt(position) - statement.offset,
line: position.lineNumber - statement.line + 1,
column: position.column - 1, // Columns are zero-based in ANTLR4.
currentSchema: context.currentSchema,
};
this.pool.runTask(infoData).then((taskId: number, result: ILanguageWorkerResultData): void => {
if (!model.isDisposed() && result.completions) {
// Determine what has been written so far, by going back from the current position until we find
// a whitespace character or the start of the line.
const line = model.getLineContent(position.lineNumber);
let index = position.column - 1;
while (index > 0 && !/[\s.]/.test(line[index - 1])) {
--index;
}
const replaceRange = context.fromLocal({
startLineNumber: position.lineNumber,
startColumn: index + 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
this.transformCompletionItems(result.completions, replaceRange).then((suggestions) => {
if (suggestions.length === 0) {
// Add a special item here if nothing was found.
// Otherwise we get some meaningless default suggestions.
suggestions.push({
label: "No Suggestions",
kind: languages.CompletionItemKind.Text,
range: replaceRange,
insertText: "",
});
}
resolve({
incomplete: false,
suggestions,
});
if (model.symbols) {
this.localSymbols.removeDependency(model.symbols);
}
}).catch((reason) => {
reject(reason);
});
} else {
resolve(undefined);
}
});
});
}
private transformCompletionItems = async (data: ICompletionData, range: IRange): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
const uppercaseKeywords = Settings.get("dbEditor.upperCaseKeywords", true);
let sortKey = this.sortKeys.get(LanguageCompletionKind.Keyword)!;
data.keywords.forEach((keyword) => {
if (!uppercaseKeywords) {
keyword = keyword.toLowerCase();
}
result.push({
label: keyword,
kind: mapCompletionKind.get(LanguageCompletionKind.Keyword)!,
range,
insertText: keyword,
sortText: sortKey + keyword,
documentation: "Keyword",
});
});
sortKey = this.sortKeys.get(LanguageCompletionKind.Function)!;
data.functions.forEach((functionName) => {
functionName = functionName.toLowerCase();
result.push({
label: functionName,
kind: mapCompletionKind.get(LanguageCompletionKind.Keyword)!,
range,
insertText: functionName,
sortText: sortKey + functionName,
documentation: "Function",
});
});
// The DB objects list can contain duplicates, in the sense that it contains multiple entries for the same
// parent objects. There's no sense in processing the same combination of completion kind, schemas + tables
// more than once.
// Keep in mind however that in the final list there can still be duplicate entries, for example
// columns from different tables which have the same name.
const handledKinds: ICompletionObjectDetails[] = [];
const findHandledKind = (details: ICompletionObjectDetails): boolean => {
const index = handledKinds.findIndex((candidate) => {
return deepEqual(candidate, details);
});
return index > -1;
};
// Sort the objects so that parent elements are handled before the sub elements (schema before tables etc.).
// This way also all symbols of the same type are listed together.
data.dbObjects.sort((lhs, rhs) => {
return lhs.kind - rhs.kind;
});
for await (const entry of data.dbObjects) {
switch (entry.kind) {
case LanguageCompletionKind.Procedure:
case LanguageCompletionKind.Function:
case LanguageCompletionKind.Udf:
case LanguageCompletionKind.Trigger:
case LanguageCompletionKind.Table:
case LanguageCompletionKind.View: {
if (entry.schemas) {
if (!findHandledKind(entry)) {
handledKinds.push(entry);
const list = await this.collectItemsFromSchemas(entry.kind, entry.schemas, range);
result.push(...list);
}
}
break;
}
case LanguageCompletionKind.Column: {
if (entry.schemas && entry.tables) {
if (!findHandledKind(entry)) {
handledKinds.push(entry);
const list = await this.collectColumns(entry.schemas, entry.tables, range);
result.push(...list);
}
}
break;
}
case LanguageCompletionKind.SystemFunction: {
if (!findHandledKind(entry)) {
// Special handling for system functions as we have individual descriptions for each of them.
handledKinds.push(entry);
const list = await this.collectSystemFunctions(range);
result.push(...list);
}
break;
}
case LanguageCompletionKind.UserVariable: {
if (!findHandledKind(entry)) {
handledKinds.push(entry);
const list = await this.collectUserVariables(range);
result.push(...list);
}
break;
}
case LanguageCompletionKind.SystemVariable: {
if (!findHandledKind(entry)) {
handledKinds.push(entry);
const list = await this.collectSystemVariables(range);
result.push(...list);
}
break;
}
default: {
if (!findHandledKind(entry)) {
handledKinds.push(entry);
const list = await this.collectItems(this.localSymbols, entry.kind, range);
result.push(...list);
}
break;
}
}
}
return result;
};
/**
* Collects completion items of the specified type.
*
* @param parent The outer scope which contains the requested symbols.
* @param kind The type of item to collect.
* @param range The replacement range for the items.
* @param documentation A string describing the item type.
*
* @returns The list of collected items.
*/
private collectItems = async (parent: ScopedSymbol, kind: LanguageCompletionKind, range: IRange,
documentation?: string): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
const addParens = kind === LanguageCompletionKind.Procedure || kind === LanguageCompletionKind.Function
|| kind === LanguageCompletionKind.Udf;
const sortKey = this.sortKeys.get(kind) || "";
if (!documentation) {
documentation = this.descriptionMap.get(kind) || "<no description available>";
}
const symbols = await this.getSymbolsOfKind(parent, kind);
for (const symbol of symbols) {
const text = symbol.name + (addParens ? "()" : "");
result.push({
label: text,
kind: mapCompletionKind.get(kind)!,
range,
insertText: text,
sortText: sortKey + text,
documentation,
});
}
return result;
};
/**
* Like `collectItems` but for specific schemas only.
*
* @param kind The type of item to collect.
* @param schemas A list of schemas to consider.
* @param range The replacement range for the items.
* @param documentation A string describing the item type.
*
* @returns The list of collected items.
*/
private collectItemsFromSchemas = async (kind: LanguageCompletionKind, schemas: Set<string>,
range: IRange, documentation?: string): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
// Load the schema list if we haven't loaded it yet.
if (!this.loadedSchemaTables.has(schemaKey)) {
await this.getSymbolsOfKind(this.localSymbols, LanguageCompletionKind.Schema);
}
for (const schema of schemas) {
const schemaSymbol = await this.localSymbols.resolve(schema, false);
if (!schemaSymbol) {
continue;
}
result.push(...await this.collectItems(schemaSymbol as ScopedSymbol, kind, range, documentation));
}
return result;
};
/**
* Collects columns for the given tables in the given schemas.
*
* @param schemas The schemas from which the tables are determined.
* @param tables The tables from which the columns are to be determined.
* @param range The replacement range for the items.
*
* @returns A list of column items.
*/
private collectColumns = async (schemas: Set<string>, tables: Set<string>,
range: IRange): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
// Load the schema list if we haven't loaded it yet.
if (!this.loadedSchemaTables.has(schemaKey)) {
await this.getSymbolsOfKind(this.localSymbols, LanguageCompletionKind.Schema);
}
for (const schema of schemas) {
const schemaSymbol = await this.localSymbols.resolve(schema, false);
if (!schemaSymbol) {
continue;
}
// Load the table list if we haven't loaded it yet.
if (!this.loadedSchemaTables.has(schema)) {
await this.getSymbolsOfKind(this.localSymbols, LanguageCompletionKind.Table);
}
for (const table of tables) {
const tableSymbol = await schemaSymbol.resolve(table, true);
if (!tableSymbol) {
continue;
}
result.push(...await this.collectItems(tableSymbol as ScopedSymbol, LanguageCompletionKind.Column,
range));
}
}
return result;
};
/**
* Collects the system function items.
*
* @param range The replacement range for the items.
*
* @returns A list of column items.
*/
private collectSystemFunctions = async (range: IRange): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
const sortKey = this.sortKeys.get(LanguageCompletionKind.SystemFunction)!;
const symbols = await this.localSymbols.getAllSymbols(SystemFunctionSymbol);
for (const symbol of symbols) {
const text = symbol.name + "()";
result.push({
label: text,
kind: mapCompletionKind.get(LanguageCompletionKind.SystemFunction)!,
range,
insertText: text,
sortText: sortKey + text,
documentation: symbol.description[1],
});
}
return result;
};
/**
* Collects the user variable items.
*
* @param range The replacement range for the items.
*
* @returns A list of column items.
*/
private collectUserVariables = async (range: IRange): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
const symbols = await this.localSymbols.getAllSymbols(UserVariableSymbol);
const sortKey = this.sortKeys.get(LanguageCompletionKind.UserVariable)!;
for (const symbol of symbols) {
const name = "@" + symbol.name;
result.push({
label: name,
kind: mapCompletionKind.get(LanguageCompletionKind.UserVariable)!,
range,
insertText: name,
sortText: sortKey + name,
documentation: "User Variable",
});
}
return result;
};
/**
* Collects the system variable items.
*
* @param range The replacement range for the items.
*
* @returns A list of column items.
*/
private collectSystemVariables = async (range: IRange): Promise<CompletionItem[]> => {
const result: CompletionItem[] = [];
const symbols = await this.localSymbols.getAllSymbols(SystemVariableSymbol);
const sortKey = this.sortKeys.get(LanguageCompletionKind.SystemVariable)!;
for (const symbol of symbols) {
const name = "@@" + symbol.name;
result.push({
label: name,
kind: mapCompletionKind.get(LanguageCompletionKind.SystemVariable)!,
range: {
startColumn: range.startColumn - 2,
startLineNumber: range.startLineNumber,
endColumn: range.endColumn,
endLineNumber: range.endLineNumber,
},
insertText: name,
sortText: sortKey + name,
documentation: (symbol).description[1],
});
}
return result;
};
/**
* Returns all symbols of the given kind from the symbol table.
*
* @param parent The outer scope which contains the requested symbols.
* @param kind The kind of the symbols to return.
*
* @returns A set with the found symbols.
*/
private getSymbolsOfKind(parent: ScopedSymbol, kind: LanguageCompletionKind): Promise<BaseSymbol[]> {
switch (kind) {
case LanguageCompletionKind.Schema: {
this.loadedSchemaTables.add(schemaKey);
return parent.getAllSymbols(SchemaSymbol);
}
case LanguageCompletionKind.Table: {
this.loadedSchemaTables.add(parent.name);
return parent.getAllSymbols(TableSymbol);
}
case LanguageCompletionKind.Index: {
return parent.getAllSymbols(IndexSymbol);
}
case LanguageCompletionKind.Column: {
return parent.getAllSymbols(ColumnSymbol);
}
case LanguageCompletionKind.View: {
return parent.getAllSymbols(ViewSymbol);
}
case LanguageCompletionKind.Label: {
return parent.getAllSymbols(LabelSymbol);
}
case LanguageCompletionKind.SystemFunction: {
return parent.getAllSymbols(SystemFunctionSymbol);
}
case LanguageCompletionKind.Function: {
return parent.getAllSymbols(StoredFunctionSymbol);
}
case LanguageCompletionKind.Procedure: {
return parent.getAllSymbols(StoredProcedureSymbol);
}
case LanguageCompletionKind.Udf: {
return parent.getAllSymbols(UdfSymbol);
}
case LanguageCompletionKind.Engine: {
return parent.getAllSymbols(EngineSymbol);
}
case LanguageCompletionKind.Tablespace: {
return parent.getAllSymbols(TablespaceSymbol);
}
case LanguageCompletionKind.UserVariable: {
return parent.getAllSymbols(UserVariableSymbol);
}
case LanguageCompletionKind.SystemVariable: {
return parent.getAllSymbols(SystemVariableSymbol);
}
case LanguageCompletionKind.Charset: {
return parent.getAllSymbols(CharsetSymbol);
}
case LanguageCompletionKind.Collation: {
return parent.getAllSymbols(CollationSymbol);
}
case LanguageCompletionKind.Event: {
return parent.getAllSymbols(EventSymbol);
}
case LanguageCompletionKind.User: {
return parent.getAllSymbols(UserSymbol);
}
case LanguageCompletionKind.Trigger: {
return parent.getAllSymbols(TriggerSymbol);
}
case LanguageCompletionKind.LogfileGroup: {
return parent.getAllSymbols(LogfileGroupSymbol);
}
case LanguageCompletionKind.Plugin: {
return parent.getAllSymbols(PluginSymbol);
}
default: {
return Promise.resolve([]);
}
}
}
}