UNPKG

rhombic

Version:

SQL parsing, lineage extraction and manipulation

356 lines 18.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.printFilter = exports.needToBeEscaped = exports.LineageHelper = exports.antlr = void 0; const SqlParser_1 = require("./SqlParser"); const HasFromVisitor_1 = require("./visitors/HasFromVisitor"); const ProjectionItemsVisitor_1 = require("./visitors/ProjectionItemsVisitor"); const insertText_1 = require("./utils/insertText"); const TablePrimaryVisitor_1 = require("./visitors/TablePrimaryVisitor"); const replaceText_1 = require("./utils/replaceText"); const getRange_1 = require("./utils/getRange"); const needToBeEscaped_1 = require("./utils/needToBeEscaped"); Object.defineProperty(exports, "needToBeEscaped", { enumerable: true, get: function () { return needToBeEscaped_1.needToBeEscaped; } }); const printFilter_1 = require("./utils/printFilter"); Object.defineProperty(exports, "printFilter", { enumerable: true, get: function () { return printFilter_1.printFilter; } }); const getText_1 = require("./utils/getText"); const OrderByVisitor_1 = require("./visitors/OrderByVisitor"); const FilterTreeVisitor_1 = require("./visitors/FilterTreeVisitor"); const WhereVisitor_1 = require("./visitors/WhereVisitor"); const getImageFromChildren_1 = require("./utils/getImageFromChildren"); const fixOrderItem_1 = require("./utils/fixOrderItem"); const removeUnusedOrderItems_1 = require("./utils/removeUnusedOrderItems"); // Antlr parser version var antlr_1 = require("./antlr"); Object.defineProperty(exports, "antlr", { enumerable: true, get: function () { return __importDefault(antlr_1).default; } }); var LineageHelper_1 = require("./LineageHelper"); Object.defineProperty(exports, "LineageHelper", { enumerable: true, get: function () { return LineageHelper_1.LineageHelper; } }); const rhombic = { /** * Parse a sql statement. * * @param sql */ parse(sql) { return parsedSql(sql); }, /** * Returns `true` if the query is empty (no executable sql) * * Note: the sql don't have to be valid, we are just checking if everything is commented * @param sql */ isEmpty(sql) { return sql.split("\n").reduce((isEmpty, line) => { if (line.trim().startsWith("--") || line.trim() === "") return isEmpty; return false; }, true); }, /** * Returns `true` if the filter is valid. * * @param filter */ isFilterValid(filter) { const { lexErrors, parseErrors } = SqlParser_1.parseFilter(filter); return lexErrors.length + parseErrors.length === 0; } }; /** * Parsed sql statement, with all utilities methods assigned. * * @param sql */ const parsedSql = (sql) => { const { cst, lexErrors, parseErrors } = SqlParser_1.parseSql(sql); if (lexErrors.length) { throw new Error(`Lexer error:\n - ${lexErrors.map(err => err.message).join("\n - ")}`); } if (parseErrors.length) { throw new Error(`Parse error:\n - ${parseErrors.map(err => err.message).join("\n - ")}`); } return { toString() { return sql; }, cst, hasFrom() { const visitor = new HasFromVisitor_1.HasFromVisitor(); visitor.visit(cst); return visitor.hasFrom; }, hasTablePrimary(name) { const visitor = new TablePrimaryVisitor_1.TablePrimaryVisitor(name); visitor.visit(cst); return visitor.hasTablePrimary; }, getTablePrimaries() { const visitor = new TablePrimaryVisitor_1.TablePrimaryVisitor(); visitor.visit(cst); return visitor.tables; }, getProjectionItem({ columns, index }, internalVisitor) { const visitor = internalVisitor || new ProjectionItemsVisitor_1.ProjectionItemsVisitor(); if (!internalVisitor) visitor.visit(cst); const projectionItems = visitor.output; if (visitor.asteriskCount > 0) { // Projection not in asterisk if (projectionItems[index] && !projectionItems[index].isAsterisk) { return { expression: projectionItems[index].expression, path: projectionItems[index].path, alias: projectionItems[index].alias, cast: projectionItems[index].cast, fn: projectionItems[index].fn, sort: projectionItems[index].sort, range: projectionItems[index].range }; } // Check for duplicate projection names const value = columns[index]; const otherNames = projectionItems.reduce((mem, i) => { if (i.isAsterisk) return mem; return [...mem, i.alias || i.expression]; }, []); const candidates = otherNames.filter(i => value.startsWith(i)).sort((a, b) => b.length - a.length); const originalValue = candidates[0]; const sort = visitor.sort.find(i => i.expression === (originalValue || value)); return { expression: originalValue || value, path: { columnName: originalValue || value }, sort: sort ? { order: sort.order || "asc", nullsOrder: sort.nullsOrder } : undefined }; } else { return { expression: projectionItems[index].expression, path: projectionItems[index].path, alias: projectionItems[index].alias, cast: projectionItems[index].cast, fn: projectionItems[index].fn, sort: projectionItems[index].sort, range: projectionItems[index].range }; } }, getProjectionItems(columns) { const visitor = new ProjectionItemsVisitor_1.ProjectionItemsVisitor(); visitor.visit(cst); return columns.map((_, index) => this.getProjectionItem({ columns, index }, visitor)); }, addProjectionItem(projectionItem, options) { // Default options options = Object.assign({ removeAsterisk: true, escapeReservedKeywords: true }, options); const visitor = new ProjectionItemsVisitor_1.ProjectionItemsVisitor(); visitor.visit(cst); const lastProjectionItem = visitor.output[visitor.output.length - 1]; const hasAsterisk = visitor.asteriskCount; // escape reserved keywords if (options.escapeReservedKeywords && projectionItem[0] !== '"' && needToBeEscaped_1.needToBeEscaped(projectionItem)) { projectionItem = `"${projectionItem}"`; } // add alias if (options.alias) { projectionItem += needToBeEscaped_1.needToBeEscaped(options.alias) ? ` AS "${options.alias}"` : ` AS ${options.alias}`; } // multiline query if (visitor.output.length > 1) { const previousProjectionItem = visitor.output[visitor.output.length - 2]; const isMultiline = previousProjectionItem.range.endLine !== lastProjectionItem.range.endLine; if (isMultiline) { const spaces = " ".repeat((lastProjectionItem.range.startColumn || 1) - 1); let nextSql = insertText_1.insertText(sql, `,\n${spaces}${projectionItem}`, { line: lastProjectionItem.range.endLine || 1, column: lastProjectionItem.range.endColumn || 0 }); if (options.removeAsterisk && hasAsterisk) { nextSql = nextSql.replace("*,\n" + spaces, ""); } return parsedSql(nextSql); } } // one line case insertion let nextSql = insertText_1.insertText(sql, `, ${projectionItem}`, { line: lastProjectionItem.range.endLine || 1, column: lastProjectionItem.range.endColumn || 0 }); if (options.removeAsterisk && hasAsterisk) { nextSql = nextSql.replace("*, ", ""); } return parsedSql(nextSql); }, updateProjectionItem({ index, value, columns }) { const visitor = new ProjectionItemsVisitor_1.ProjectionItemsVisitor(); visitor.visit(cst); const projectionItems = visitor.output; const asteriskIndex = projectionItems.findIndex(t => t.isAsterisk) || 0; if (visitor.asteriskCount > 0 && index >= asteriskIndex) { // Expand asterisk const nonAsteriskItemsCount = projectionItems.filter(i => !i.isAsterisk).length; const projectionItemsBehindAsterisk = (columns.length - nonAsteriskItemsCount) / visitor.asteriskCount; // Extract tableAlias const tablePrimaries = this.getTablePrimaries(); const tableAliases = tablePrimaries.reduce((mem, i) => (i.alias ? [...mem, i.alias] : mem), []); const nextSql = replaceText_1.replaceText(sql, columns .slice(asteriskIndex, asteriskIndex + projectionItemsBehindAsterisk) .map((c, i) => { if (i + asteriskIndex === index) return value; // Deal with tableAlias, `a.my_column` should not be espaced! const aliasCase = tableAliases.reduce((mem, alias) => { if (c.startsWith(`${alias}.`)) { c = c.slice(`${alias}.`.length); return `${alias}.${needToBeEscaped_1.needToBeEscaped(c) ? `"${c}"` : c}`; } return mem; }, ""); if (aliasCase) return aliasCase; return needToBeEscaped_1.needToBeEscaped(c) ? `"${c}"` : c; }) .join(", "), projectionItems[asteriskIndex].range); return parsedSql(nextSql); } else { const targetNode = projectionItems[index]; const nextSql = replaceText_1.replaceText(sql, value, targetNode.range); // Check if we need to rename an order item const needToFixOrderItem = visitor.sort.filter(i => i.expression === targetNode.expression || i.expression === targetNode.alias).length > 0; if (needToFixOrderItem) { return parsedSql(fixOrderItem_1.fixOrderItem(parsedSql(nextSql), targetNode, index)); } return parsedSql(nextSql); } }, removeProjectionItem({ columns, index }) { const visitor = new ProjectionItemsVisitor_1.ProjectionItemsVisitor(); visitor.visit(cst); const projectionItems = visitor.output; const hasSort = visitor.sort.length > 0; if (visitor.asteriskCount > 0) { // Expand asterisk const nonAsteriskItemsCount = projectionItems.filter(i => !i.isAsterisk).length; const projectionItemsBehindAsterisk = (columns.length - nonAsteriskItemsCount) / visitor.asteriskCount; const asteriskIndex = projectionItems.findIndex(t => t.isAsterisk) || 0; if (index >= asteriskIndex) { const nextSql = replaceText_1.replaceText(sql, columns .slice(asteriskIndex, asteriskIndex + projectionItemsBehindAsterisk) .filter((_, i) => i + asteriskIndex !== index) .join(", "), projectionItems[asteriskIndex].range); if (hasSort) { return parsedSql(removeUnusedOrderItems_1.removeUnusedOrderItems(parsedSql(nextSql), columns[index])); } return parsedSql(nextSql); } } const targetNode = projectionItems[index]; if (visitor.commas.length > 0) { // Include the commas in the selection to remove const comma = getRange_1.getRange(visitor.commas[Math.min(visitor.commas.length - 1, index)]); if (comma.startLine < targetNode.range.startLine || comma.startColumn < targetNode.range.startColumn) { targetNode.range.startLine = comma.startLine || targetNode.range.startLine; targetNode.range.startColumn = comma.startColumn || targetNode.range.startColumn; } else { targetNode.range.endLine = comma.endLine || targetNode.range.endLine; targetNode.range.endColumn = comma.endColumn || targetNode.range.endColumn; // Remove extra space const textToRemove = getText_1.getText(sql, Object.assign(Object.assign({}, targetNode.range), { endColumn: targetNode.range.endColumn + 1 })); if (textToRemove[textToRemove.length - 1] === " ") { targetNode.range.endColumn++; } } } const isLastProjectionItem = projectionItems.length === 1; let nextSql = replaceText_1.replaceText(sql, isLastProjectionItem ? "*" : "", targetNode.range); // Remove resulting emtpy line const removedLine = targetNode.range.startLine - 1; if (!nextSql.split("\n")[removedLine].trim()) { nextSql = [...nextSql.split("\n").slice(0, removedLine), ...nextSql.split("\n").slice(removedLine + 1)].join("\n"); } if (hasSort) { return parsedSql(removeUnusedOrderItems_1.removeUnusedOrderItems(parsedSql(nextSql), columns[index]).trim()); } return parsedSql(nextSql); }, orderBy({ expression, order, nullsOrder }) { const visitor = new OrderByVisitor_1.OrderByVisitor(); visitor.visit(cst); const orderItems = visitor.output; if (orderItems.length === 0) { // Add order by statement let orderBy = ` ORDER BY ${expression}`; if (order) orderBy += ` ${order.toUpperCase()}`; if (nullsOrder) orderBy += ` NULLS ${nullsOrder.toUpperCase()}`; if (visitor.insertLocation) { return parsedSql(insertText_1.insertText(sql, orderBy, visitor.insertLocation)); } return parsedSql(sql + orderBy); } else { const existingOrderItem = orderItems.find(i => i.expression === expression); if (existingOrderItem) { // Update existing order const nextNullsOrders = nullsOrder || existingOrderItem.nullsOrder; const nextSql = replaceText_1.replaceText(sql, `${existingOrderItem.expression} ${(order || (existingOrderItem.order === "desc" ? "asc" : "desc")).toUpperCase()}${nextNullsOrders ? ` NULLS ${nextNullsOrders.toUpperCase()}` : ""}`, getRange_1.getRange(existingOrderItem)); return parsedSql(nextSql); } else { // Replace order items with the new one let orderByItem = expression; if (order) orderByItem += ` ${order.toUpperCase()}`; if (nullsOrder) orderByItem += ` NULLS ${nullsOrder.toUpperCase()}`; const firstItem = orderItems[0]; const lastItem = orderItems[orderItems.length - 1]; const nextSql = replaceText_1.replaceText(sql, orderByItem, getRange_1.getRange({ startLine: firstItem.startLine, startColumn: firstItem.startColumn, endLine: lastItem.endLine, endColumn: lastItem.endColumn })); return parsedSql(nextSql); } } }, getFilterTree() { const visitor = new FilterTreeVisitor_1.FilterTreeVisitor(); visitor.visit(cst); return visitor.tree.length > 1 ? visitor.tree : []; }, getFilterString() { const visitor = new WhereVisitor_1.WhereVisitor(); visitor.visit(cst); return visitor.booleanExpressionNode ? getImageFromChildren_1.getImageFromChildren(visitor.booleanExpressionNode) : ""; }, updateFilter(filter) { const visitor = new WhereVisitor_1.WhereVisitor(); visitor.visit(cst); const hasWhere = Boolean(visitor.booleanExpressionNode); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!visitor.booleanExpression) { throw new Error("Can't update/add a filter to this query"); } const computedFilter = typeof filter === "string" ? filter : printFilter_1.printFilter(filter); const nextSql = replaceText_1.replaceText(sql, hasWhere ? computedFilter : ` WHERE ${computedFilter} `, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion hasWhere ? visitor.booleanExpressionRange : visitor.tableRange).trim(); if (computedFilter === "" && visitor.whereRange) { return parsedSql(replaceText_1.replaceText(sql, "", Object.assign(Object.assign({}, visitor.whereRange), { startColumn: visitor.whereRange.startColumn - 1 // Remove the space before WHERE })).trim()); } return parsedSql(nextSql); } }; }; exports.default = rhombic; //# sourceMappingURL=index.js.map