UNPKG

rhombic

Version:

SQL parsing, lineage extraction and manipulation

242 lines 9.22 kB
"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