rhombic
Version:
SQL parsing, lineage extraction and manipulation
242 lines • 9.22 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompletionVisitor = void 0;
const common_1 = __importDefault(require("./common"));
const QueryStructureVisitor_1 = require("./QueryStructureVisitor");
const SqlBaseParser_1 = require("./SqlBaseParser");
function availableColumns(relation) {
const columns = [];
relation.relations.forEach((rel, name) => {
const relationName = name !== rel.id ? name : undefined;
rel.columns.forEach(col => {
columns.push({
relation: relationName,
name: col.label,
desc: col.data !== undefined ? col.data : undefined
});
});
});
return columns;
}
const selectFromSnippet = {
label: "SELECT ? FROM ?",
template: "SELECT $0 FROM $1"
};
function mapMetadataLookup(getColumns) {
const getTable = (tp) => {
const columns = tp.catalogName !== undefined
? getColumns({ table: tp.tableName, catalogOrSchema: tp.catalogName, schema: tp.schemaName })
: getColumns({ table: tp.tableName, catalogOrSchema: tp.schemaName });
return {
table: { id: "", data: null },
columns: columns.map(c => {
return {
id: c.name,
data: c
};
})
};
};
return getTable;
}
/**
* The visitor responsible for computing possible completion items based on the cursor position.
*
* This class performs two related tasks: identifying the subquery (and part of the query) where
* the cursor is found in and collecting relevant details for completion at that point, such as
* visible CTEs and columns of relations in the `FROM` clause.
*
* The cursor handling is delegated to the `Cursor` instance that is provided in the constructor.
*/
class CompletionVisitor extends QueryStructureVisitor_1.QueryStructureVisitor {
/**
* @param cursor Utility functions to identify the cursor within the query
* @param getTables Fetch a list of available tables (to present as completion items)
* @param getTable Fetch details (such as available columns) for a table
*/
constructor(cursor, getColumns) {
super(mapMetadataLookup(getColumns));
this.cursor = cursor;
this.hasCompletions = false;
this.completions = { type: "other", snippets: [] };
}
defaultResult() {
return;
}
aggregateResult(_cur, _next) {
return;
}
getSuggestions() {
return this.completions;
}
/**
* Checks if tyhe provided query is the innermost query containing the cursor.
* If so, compute the completion suggestions for that query.
*
* @param relation the current relation to consider
*/
updateCompletionItems(relation) {
var _a, _b;
// Is this the innermost query with the cursor inside
if (!this.hasCompletions && this.caretScope !== undefined) {
this.hasCompletions = true;
switch (this.caretScope.type) {
case "select-column":
case "spec-column":
this.completions = {
type: "column",
columns: availableColumns(relation),
snippets: this.completions.snippets
};
break;
case "scoped-column": {
const relationName = this.caretScope.relation;
const newCompletions = (_b = (_a = relation.findLocalRelation({ name: relationName, quoted: false })) === null || _a === void 0 ? void 0 : _a.columns.map(c => {
return {
relation: relationName,
name: c.label
};
})) !== null && _b !== void 0 ? _b : [];
this.completions = { type: "column", columns: newCompletions, snippets: this.completions.snippets };
break;
}
case "relation": {
const ctes = relation.getCTENames();
this.completions = {
type: "relation",
incompleteReference: this.caretScope.prefix.length > 0 ? { references: this.caretScope.prefix } : undefined,
relations: ctes,
snippets: this.completions.snippets
};
break;
}
}
}
}
/**
* In case a query has been completely handled by this visitor we may need to update the completions.
*
* This is called for the whole query (including query organization features). For simple query terms
* (like the individual parts of a UNION query) are handled by the `visitQueryTermDefault` method.
*
* @param relation
* @param _alias
* @returns
*/
onRelation(relation, _alias) {
if (relation instanceof QueryStructureVisitor_1.TableRelation) {
return;
}
this.updateCompletionItems(relation);
}
/**
* In case we handled a query term we need to potentially update the completions.
*
* This is called for query terms e.g. within set operations. We need this in addition
* to `onRelation` to make sure we can complete in the individual sub-queries of set
* operations.
*
* @param ctx
*/
visitQueryTermDefault(ctx) {
super.visitQueryTermDefault(ctx);
this.updateCompletionItems(this.currentRelation);
}
/**
* This method handles the case of an empty input to then provide the SELECTFROM snippet as
* a completion item.
*
* @param node
*/
visitErrorNode(node) {
var _a;
super.visitErrorNode(node);
if (this.cursor.isEqualTo((_a = node.symbol.text) !== null && _a !== void 0 ? _a : "") && node.parent instanceof SqlBaseParser_1.StatementContext) {
this.completions.snippets.push(selectFromSnippet);
this.caretScope = { type: "other" };
}
}
/**
* Handle cases where the cursor is at the end of a table name (prefix).
*
* @param ctx
*/
visitTableName(ctx) {
const multipartTableName = ctx
.multipartIdentifier()
.errorCapturingIdentifier()
.map(v => common_1.default.stripQuote(v.identifier()).name);
const lastPart = multipartTableName[multipartTableName.length - 1];
if (this.cursor.isSuffixOf(lastPart)) {
this.caretScope = { type: "relation", prefix: multipartTableName.slice(0, -1) };
}
super.visitTableName(ctx);
}
/**
* Handle cases where the cursor is in the position of a table name, with no prefix provided.
*
* @param ctx
*/
visitAliasedRelation(ctx) {
var _a;
this.visitChildren(ctx);
const relation = ctx.relation();
if (relation.start === relation.stop && this.cursor.isEqualTo((_a = relation.start.text) !== null && _a !== void 0 ? _a : "")) {
this.completions.snippets.push(selectFromSnippet);
this.caretScope = { type: "relation", prefix: [] };
}
}
/**
* Handle cases where the cursor is in the position of a column. We distinguish between
* the cursor in a SELECT clause and in any other position (e.h. WHERE clause).
* @param ctx
*/
visitColumnReference(ctx) {
super.visitColumnReference(ctx);
if (this.cursor.isSuffixOf(ctx.identifier().text)) {
if (this.currentRelation.currentClause === "select") {
this.caretScope = { type: "select-column" };
}
else {
this.caretScope = { type: "spec-column" };
}
}
}
/**
* Handle cases, where the cursor is right after a dot to suggest columns for the relevant prefix.
*
* @param ctx
*/
visitDereference(ctx) {
super.visitDereference(ctx);
if (this.cursor.isSuffixOf(ctx.identifier().text)) {
const ns = ctx.primaryExpression();
if (ns instanceof SqlBaseParser_1.ColumnReferenceContext) {
this.caretScope = { type: "scoped-column", relation: ns.identifier().text };
}
else {
this.caretScope = { type: "other" };
}
}
}
extractTableAndColumn(ctx) {
const col = super.extractTableAndColumn(ctx);
if (col !== undefined) {
if (this.cursor.isSuffixOf(col.column.name)) {
const name = this.cursor.removeFrom(col.column.name);
if (name.length == 0) {
return undefined;
}
else {
col.column.name = name;
}
}
}
return col;
}
}
exports.CompletionVisitor = CompletionVisitor;
//# sourceMappingURL=CompletionVisitor.js.map