rhombic
Version:
SQL parsing, lineage extraction and manipulation
356 lines • 18.4 kB
JavaScript
;
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