From b27d7541d2b1e6d912630e6dcd0eb568e19244d9 Mon Sep 17 00:00:00 2001 From: surister Date: Fri, 14 Jun 2024 12:30:51 +0200 Subject: [PATCH] Javascript: Implement `ExceptionCollectorListener` and make it default behavior --- .../cratedb_sqlparse/parser.js | 134 ++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/cratedb_sqlparse_js/cratedb_sqlparse/parser.js b/cratedb_sqlparse_js/cratedb_sqlparse/parser.js index 90c6ef0..af1d10e 100644 --- a/cratedb_sqlparse_js/cratedb_sqlparse/parser.js +++ b/cratedb_sqlparse_js/cratedb_sqlparse/parser.js @@ -8,7 +8,6 @@ function BEGIN_DOLLAR_QUOTED_STRING_action(localctx, actionIndex) { } } - function END_DOLLAR_QUOTED_STRING_action(localctx, actionIndex) { if (actionIndex === 1) { this.tags.pop(); @@ -28,6 +27,44 @@ SqlBaseLexer.prototype.END_DOLLAR_QUOTED_STRING_sempred = END_DOLLAR_QUOTED_STRI export class ParseError extends Error { name = 'ParseError' + + constructor(message, offending_token) { + super(message); + this.msg = message; + this.offending_token = offending_token; + this.line = this.getLine(); + this.column = this.getColumn(); + this.message = this.error_message(); + } + + error_message() { + return `[line ${this.line}:${this.column} ${this.message}]` + } + + getColumn() { + return this.offending_token.column + } + + getLine() { + return this.offending_token.line + } + + getOriginalQueryWithErrorMarked() { + const query = this.offending_token.source[1].strdata + const offendingTokenText = query.substring(this.offending_token.start, this.offending_token.stop + 1) + const queryLines = query.split("\n") + const offendingLine = queryLines[this.getLine() - 1] + const newLineOffset = offendingLine.indexOf(offendingTokenText) + + const newline = ( + offendingLine + + "\n" + + ("‎".repeat(newLineOffset) + "^".repeat(this.offending_token.stop - this.offending_token.start + 1)) + ) + queryLines[this.line - 1] = newline + + return queryLines.join("\n") + } } class CaseInsensitiveStream extends InputStream { @@ -42,26 +79,93 @@ class CaseInsensitiveStream extends InputStream { class ExceptionErrorListener extends ErrorListener { syntaxError(recognizer, offendingSymbol, line, column, msg, e) { - throw new ParseError(`line${line}:${column} ${msg}`); + throw new ParseError( + msg, + offendingSymbol, + e, + e.ctx.parser.getTokenStream().getText(e.ctx.start, e.offendingToken.tokenIndex) + ) + } +} + +class ExceptionCollectorListener extends ErrorListener { + constructor() { + super(); + this.errors = []; + } + + syntaxError(recognizer, offendingSymbol, line, column, msg, e) { + super.syntaxError(recognizer, offendingSymbol, line, column, msg, e); + const error = new ParseError( + msg, + offendingSymbol, + e, + e.ctx.parser.getTokenStream().getText(e.ctx.start, e.offendingToken.tokenIndex) + ) + this.errors.push(error) + } +} + +class Metadata { + /* + * Represents the metadata of the query, the actual interesting parts of the query such as: + * table, schema, columns, options... + */ + constructor(schema, table, parameterized_properties, with_properties) { + this.schema = schema + this.table = table + this.parameterized_properties = parameterized_properties + this.with_properties = with_properties + } + + toString() { + } } class Statement { - constructor(ctx) { + /* + * Represents a CrateDB SQL statement. + * */ + constructor(ctx, exception) { this.query = ctx.parser.getTokenStream().getText( new Interval( ctx.start.tokenIndex, ctx.stop.tokenIndex, ) ) - this.original_query = ctx.parser.getTokenStream().getText() - this.tree = ctx.toStringTree(null, ctx.parser) - this.type = ctx.start.text - this.ctx = ctx + this.original_query = ctx.parser.getTokenStream().getText(); + this.tree = ctx.toStringTree(null, ctx.parser); + this.type = ctx.start.text; + this.ctx = ctx; + self.exception = exception; + self.metadata = new Metadata(); } } -export function sqlparse(query) { +function trim(string) { + return string.replace(/^\s+|\s+$/gm, ''); +} + + +function findSuitableError(statement, errors) { + for (const error of errors) { + let error_query = error.query; + if (error_query.endsWith(";")) { + error_query = error_query.substring(0, error_query.length - 1); + } + + error_query = trim(error_query); + + // If a good match error_query contains statement.query + if (error_query.contains(statement.query)) { + statement.exception = error; + errors.unshift(error); + } + } +} + +export function sqlparse(query, raise_exception = false) { const input = new CaseInsensitiveStream(query); const lexer = new SqlBaseLexer(input); lexer.removeErrorListeners(); @@ -69,9 +173,17 @@ export function sqlparse(query) { const parser = new SqlBaseParser(stream); parser.removeErrorListeners(); - parser.addErrorListener(new ExceptionErrorListener()); + + const errorListener = raise_exception ? new ExceptionErrorListener() : new ExceptionCollectorListener() + parser.addErrorListener(errorListener); const tree = parser.statements(); - return tree.children.filter((children) => children instanceof SqlBaseParser.StatementContext).map((children) => new Statement(children)) -} + const statementsContext = tree.children.filter((children) => children instanceof SqlBaseParser.StatementContext) + + let statements = [] + for (const statementContext of statementsContext) { + let stmt = new Statement(statementContext) + findSuitableError(stmt, errorListener.errors) + } +} \ No newline at end of file