diff --git a/src/locale/locale.ts b/src/locale/locale.ts new file mode 100644 index 00000000..4f993acf --- /dev/null +++ b/src/locale/locale.ts @@ -0,0 +1,46 @@ +const zh_CN = { + stmtInComplete: '语句不完整', + noValidPosition: '在此位置无效', + expecting: ',期望', + unfinishedMultilineComment: '未完成的多行注释', + unfinishedDoubleQuoted: '未完成的双引号字符串字变量', + unfinishedSingleQuoted: '未完成的单引号字符串字变量', + unfinishedTickQuoted: '未完成的反引号引用字符串字变量', + noValidInput: '没有有效的输入', + newObj: '一个新的对象', + existingObj: '一个存在的对象', + new: '一个新的', + existing: '一个存在的', + orKeyword: '或者一个关键字', + keyword: '一个关键字', + missing: '缺少', + at: '在', + or: '或者', +}; + +const en_US: typeof zh_CN = { + stmtInComplete: 'Statement is incomplete', + noValidPosition: 'is not valid at this position', + expecting: ', expecting ', + unfinishedMultilineComment: 'Unfinished multiline comment', + unfinishedDoubleQuoted: 'Unfinished double quoted string literal', + unfinishedSingleQuoted: 'Unfinished single quoted string literal', + unfinishedTickQuoted: 'Unfinished back tick quoted string literal', + noValidInput: 'is no valid input at all', + newObj: 'a new object', + existingObj: 'an existing object', + new: 'a new ', + existing: 'an existing ', + orKeyword: ' or a keyword', + keyword: 'a keyword', + missing: 'missing ', + at: ' at ', + or: ' or ', +}; + +const i18n = { + zh_CN, + en_US, +}; + +export { i18n }; diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index 31082f6d..8db50605 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -8,13 +8,14 @@ import { ParseTreeWalker, ParseTreeListener, PredictionMode, + ANTLRErrorListener, } from 'antlr4ng'; import { CandidatesCollection, CodeCompletionCore } from 'antlr4-c3'; import { SQLParserBase } from '../../lib/SQLParserBase'; import { findCaretTokenIndex } from './findCaretTokenIndex'; import { ctxToText, tokenToWord, WordRange, TextSlice } from './textAndWord'; -import { CaretPosition, Suggestions, SyntaxSuggestion } from './types'; -import { ParseError, ErrorListener, ParseErrorListener } from './parseErrorListener'; +import { CaretPosition, LOCALE_TYPE, Suggestions, SyntaxSuggestion } from './types'; +import { ParseError, ErrorListener } from './parseErrorListener'; import { ErrorStrategy } from './errorStrategy'; import type { SplitListener } from './splitListener'; import type { EntityCollector } from './entityCollector'; @@ -78,6 +79,11 @@ export abstract class BasicSQL< */ protected abstract get splitListener(): SplitListener; + /** + * Get a new errorListener instance. + */ + protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener; + /** * Get a new entityCollector instance. */ @@ -86,6 +92,8 @@ export abstract class BasicSQL< caretTokenIndex?: number ): EntityCollector; + public locale: LOCALE_TYPE = 'en_US'; + /** * Create an antlr4 lexer from input. * @param input string @@ -95,7 +103,7 @@ export abstract class BasicSQL< const lexer = this.createLexerFromCharStream(charStreams); if (errorListener) { lexer.removeErrorListeners(); - lexer.addErrorListener(new ParseErrorListener(errorListener)); + lexer.addErrorListener(this.createErrorListener(errorListener)); } return lexer; } @@ -111,7 +119,7 @@ export abstract class BasicSQL< parser.interpreter.predictionMode = PredictionMode.SLL; if (errorListener) { parser.removeErrorListeners(); - parser.addErrorListener(new ParseErrorListener(errorListener)); + parser.addErrorListener(this.createErrorListener(errorListener)); } return parser; @@ -142,7 +150,7 @@ export abstract class BasicSQL< this._lexer = this.createLexerFromCharStream(this._charStreams); this._lexer.removeErrorListeners(); - this._lexer.addErrorListener(new ParseErrorListener(this._errorListener)); + this._lexer.addErrorListener(this.createErrorListener(this._errorListener)); this._tokenStream = new CommonTokenStream(this._lexer); /** @@ -178,7 +186,7 @@ export abstract class BasicSQL< this._parsedInput = input; parser.removeErrorListeners(); - parser.addErrorListener(new ParseErrorListener(this._errorListener)); + parser.addErrorListener(this.createErrorListener(this._errorListener)); this._parseTree = parser.program(); diff --git a/src/parser/common/parseErrorListener.ts b/src/parser/common/parseErrorListener.ts index 0a1295bc..902a36f3 100644 --- a/src/parser/common/parseErrorListener.ts +++ b/src/parser/common/parseErrorListener.ts @@ -4,7 +4,14 @@ import { ANTLRErrorListener, RecognitionException, ATNSimulator, + LexerNoViableAltException, + Lexer, + Parser, + InputMismatchException, + NoViableAltException, } from 'antlr4ng'; +import { LOCALE_TYPE } from './types'; +import { transform } from './transform'; /** * Converted from {@link SyntaxError}. @@ -39,10 +46,12 @@ export interface SyntaxError { */ export type ErrorListener = (parseError: ParseError, originalError: SyntaxError) => void; -export class ParseErrorListener implements ANTLRErrorListener { +export abstract class ParseErrorListener implements ANTLRErrorListener { private _errorListener: ErrorListener; + private locale: LOCALE_TYPE; - constructor(errorListener: ErrorListener) { + constructor(errorListener: ErrorListener, locale: LOCALE_TYPE = 'en_US') { + this.locale = locale; this._errorListener = errorListener; } @@ -52,6 +61,8 @@ export class ParseErrorListener implements ANTLRErrorListener { reportContextSensitivity() {} + protected abstract getExpectedText(parser: Parser, token: Token): string; + syntaxError( recognizer: Recognizer, offendingSymbol: Token | null, @@ -60,6 +71,87 @@ export class ParseErrorListener implements ANTLRErrorListener { msg: string, e: RecognitionException ) { + let message = ''; + // If not undefined then offendingSymbol is of type Token. + if (offendingSymbol) { + let token = offendingSymbol as Token; + const parser = recognizer as Parser; + + // judge token is EOF + const isEof = token.type === Token.EOF; + if (isEof) { + token = parser.tokenStream.get(token.tokenIndex - 1); + } + const wrongText = token.text ?? ''; + + const isInComplete = isEof && wrongText !== ' '; + + const expectedText = isInComplete ? '' : this.getExpectedText(parser, token); + + if (!e) { + // handle missing or unwanted tokens. + message = msg; + if (msg.includes('extraneous')) { + message = `'${wrongText}' {noValidPosition}${ + expectedText.length ? `{expecting}${expectedText}` : '' + }`; + } + if (msg.includes('missing')) { + const regex = /missing\s+'([^']+)'/; + const match = msg.match(regex); + message = `{missing}`; + if (match) { + const missKeyword = match[1]; + message += `'${missKeyword}'`; + } else { + message += `{keyword}`; + } + message += `{at}'${wrongText}'`; + } + } else { + // handle mismatch exception or no viable alt exception + if (e instanceof InputMismatchException || e instanceof NoViableAltException) { + if (isEof) { + message = `{stmtInComplete}`; + } else { + message = `'${wrongText}' {noValidPosition}`; + } + if (expectedText.length > 0) { + message += `{expecting}${expectedText}`; + } + } else { + message = msg; + } + } + } else { + // No offending symbol, which indicates this is a lexer error. + if (e instanceof LexerNoViableAltException) { + const lexer = recognizer as Lexer; + const input = lexer.inputStream; + let text = lexer.getErrorDisplay( + input.getText(lexer._tokenStartCharIndex, input.index) + ); + switch (text[0]) { + case '/': + message = '{unfinishedMultilineComment}'; + break; + case '"': + message = '{unfinishedDoubleQuoted}'; + break; + case "'": + message = '{unfinishedSingleQuoted}'; + break; + case '`': + message = '{unfinishedTickQuoted}'; + break; + + default: + message = '"' + text + '" {noValidInput}'; + break; + } + } + } + message = transform(message, this.locale); let endCol = charPositionInLine + 1; if (offendingSymbol && offendingSymbol.text !== null) { endCol = charPositionInLine + offendingSymbol.text.length; @@ -71,7 +163,7 @@ export class ParseErrorListener implements ANTLRErrorListener { endLine: line, startColumn: charPositionInLine + 1, endColumn: endCol + 1, - message: msg, + message, }, { e, diff --git a/src/parser/common/transform.ts b/src/parser/common/transform.ts new file mode 100644 index 00000000..f4e52317 --- /dev/null +++ b/src/parser/common/transform.ts @@ -0,0 +1,17 @@ +import { LOCALE_TYPE } from './types'; +import { i18n } from '../../locale/locale'; + +/** + * transform message to locale language + * @param message error msg + * @param locale language setting + */ +function transform(message: string, locale: LOCALE_TYPE) { + const regex = /{([^}]+)}/g; + return message.replace( + regex, + (_, key: keyof (typeof i18n)[typeof locale]) => i18n[locale][key] || '' + ); +} + +export { transform }; diff --git a/src/parser/common/types.ts b/src/parser/common/types.ts index 853673ce..ffb7bdb6 100644 --- a/src/parser/common/types.ts +++ b/src/parser/common/types.ts @@ -67,3 +67,5 @@ export interface Suggestions { */ readonly keywords: string[]; } + +export type LOCALE_TYPE = 'zh_CN' | 'en_US'; diff --git a/src/parser/flink/flinkErrorListener.ts b/src/parser/flink/flinkErrorListener.ts new file mode 100644 index 00000000..61a8a157 --- /dev/null +++ b/src/parser/flink/flinkErrorListener.ts @@ -0,0 +1,76 @@ +import { CodeCompletionCore } from 'antlr4-c3'; +import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; +import { Parser, Token } from 'antlr4ng'; +import { FlinkSqlParser } from '../../lib/flink/FlinkSqlParser'; +import { LOCALE_TYPE } from '../common/types'; + +export class FlinkErrorListener extends ParseErrorListener { + private preferredRules: Set; + + private objectNames: Map = new Map([ + [FlinkSqlParser.RULE_catalogPath, 'catalog'], + [FlinkSqlParser.RULE_catalogPathCreate, 'catalog'], + [FlinkSqlParser.RULE_databasePath, 'database'], + [FlinkSqlParser.RULE_databasePathCreate, 'database'], + [FlinkSqlParser.RULE_tablePath, 'table'], + [FlinkSqlParser.RULE_tablePathCreate, 'table'], + [FlinkSqlParser.RULE_viewPath, 'view'], + [FlinkSqlParser.RULE_viewPathCreate, 'view'], + [FlinkSqlParser.RULE_functionName, 'function'], + [FlinkSqlParser.RULE_functionNameCreate, 'function'], + [FlinkSqlParser.RULE_columnName, 'column'], + [FlinkSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case FlinkSqlParser.RULE_databasePath: + case FlinkSqlParser.RULE_tablePath: + case FlinkSqlParser.RULE_viewPath: + case FlinkSqlParser.RULE_functionName: + case FlinkSqlParser.RULE_columnName: + case FlinkSqlParser.RULE_catalogPath: { + result.push(`{existing}${name}`); + break; + } + case FlinkSqlParser.RULE_databasePathCreate: + case FlinkSqlParser.RULE_tablePathCreate: + case FlinkSqlParser.RULE_functionNameCreate: + case FlinkSqlParser.RULE_viewPathCreate: + case FlinkSqlParser.RULE_columnNameCreate: + case FlinkSqlParser.RULE_catalogPathCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join('{or}'); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/flink/index.ts b/src/parser/flink/index.ts index eb727986..61d19d22 100644 --- a/src/parser/flink/index.ts +++ b/src/parser/flink/index.ts @@ -7,6 +7,8 @@ import { BasicSQL } from '../common/basicSQL'; import { StmtContextType } from '../common/entityCollector'; import { FlinkSqlSplitListener } from './flinkSplitListener'; import { FlinkEntityCollector } from './flinkEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { FlinkErrorListener } from './flinkErrorListener'; export { FlinkSqlSplitListener, FlinkEntityCollector }; @@ -37,6 +39,10 @@ export class FlinkSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [HiveSqlParser.RULE_dbSchemaName, 'database'], + [HiveSqlParser.RULE_dbSchemaNameCreate, 'database'], + [HiveSqlParser.RULE_tableName, 'table'], + [HiveSqlParser.RULE_tableNameCreate, 'table'], + [HiveSqlParser.RULE_viewName, 'view'], + [HiveSqlParser.RULE_viewNameCreate, 'view'], + [HiveSqlParser.RULE_functionNameForDDL, 'function'], + [HiveSqlParser.RULE_functionNameForInvoke, 'function'], + [HiveSqlParser.RULE_functionNameCreate, 'function'], + [HiveSqlParser.RULE_columnName, 'column'], + [HiveSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case HiveSqlParser.RULE_dbSchemaName: + case HiveSqlParser.RULE_tableName: + case HiveSqlParser.RULE_viewName: + case HiveSqlParser.RULE_functionNameForDDL: + case HiveSqlParser.RULE_functionNameForInvoke: + case HiveSqlParser.RULE_columnName: { + result.push(`{existing}${name}`); + break; + } + case HiveSqlParser.RULE_dbSchemaNameCreate: + case HiveSqlParser.RULE_tableNameCreate: + case HiveSqlParser.RULE_functionNameCreate: + case HiveSqlParser.RULE_viewNameCreate: + case HiveSqlParser.RULE_columnNameCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join('{or}'); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/hive/index.ts b/src/parser/hive/index.ts index c0c1c8ba..7bac2468 100644 --- a/src/parser/hive/index.ts +++ b/src/parser/hive/index.ts @@ -8,6 +8,8 @@ import { EntityContextType, Suggestions, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { HiveSqlSplitListener } from './hiveSplitListener'; import { HiveEntityCollector } from './hiveEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { HiveErrorListener } from './hiveErrorListener'; export { HiveEntityCollector, HiveSqlSplitListener }; @@ -38,6 +40,10 @@ export class HiveSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [ImpalaSqlParser.RULE_databaseNamePath, 'database'], + [ImpalaSqlParser.RULE_databaseNameCreate, 'database'], + [ImpalaSqlParser.RULE_tableNamePath, 'table'], + [ImpalaSqlParser.RULE_tableNameCreate, 'table'], + [ImpalaSqlParser.RULE_viewNamePath, 'view'], + [ImpalaSqlParser.RULE_viewNameCreate, 'view'], + [ImpalaSqlParser.RULE_functionNamePath, 'function'], + [ImpalaSqlParser.RULE_functionNameCreate, 'function'], + [ImpalaSqlParser.RULE_columnNamePath, 'column'], + [ImpalaSqlParser.RULE_columnNamePathCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case ImpalaSqlParser.RULE_databaseNamePath: + case ImpalaSqlParser.RULE_tableNamePath: + case ImpalaSqlParser.RULE_functionNamePath: + case ImpalaSqlParser.RULE_viewNamePath: + case ImpalaSqlParser.RULE_columnNamePath: { + result.push(`{existing}${name}`); + break; + } + case ImpalaSqlParser.RULE_databaseNameCreate: + case ImpalaSqlParser.RULE_tableNameCreate: + case ImpalaSqlParser.RULE_functionNameCreate: + case ImpalaSqlParser.RULE_viewNameCreate: + case ImpalaSqlParser.RULE_columnNamePathCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join(`{or}`); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/impala/index.ts b/src/parser/impala/index.ts index 5afb965a..9902507e 100644 --- a/src/parser/impala/index.ts +++ b/src/parser/impala/index.ts @@ -7,6 +7,8 @@ import { EntityContextType, Suggestions, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { ImpalaSqlSplitListener } from './impalaSplitListener'; import { ImpalaEntityCollector } from './impalaEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { ImpalaErrorListener } from './ImpalaErrorListener'; export { ImpalaEntityCollector, ImpalaSqlSplitListener }; @@ -36,6 +38,10 @@ export class ImpalaSQL extends BasicSQL { return new MysqlSplitListener(); } + protected createErrorListener(_errorListener: ErrorListener) { + return new MysqlErrorListener(_errorListener, this.preferredRules, this.locale); + } + protected createEntityCollector(input: string, caretTokenIndex?: number) { return new MySqlEntityCollector(input, caretTokenIndex); } diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts new file mode 100644 index 00000000..ba6c1440 --- /dev/null +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -0,0 +1,72 @@ +import { CodeCompletionCore } from 'antlr4-c3'; +import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; +import { Parser, Token } from 'antlr4ng'; +import { MySqlParser } from '../../lib/mysql/MySqlParser'; +import { LOCALE_TYPE } from '../common/types'; + +export class MysqlErrorListener extends ParseErrorListener { + private preferredRules: Set; + + private objectNames: Map = new Map([ + [MySqlParser.RULE_databaseName, 'database'], + [MySqlParser.RULE_databaseNameCreate, 'database'], + [MySqlParser.RULE_tableName, 'table'], + [MySqlParser.RULE_tableNameCreate, 'table'], + [MySqlParser.RULE_viewName, 'view'], + [MySqlParser.RULE_viewNameCreate, 'view'], + [MySqlParser.RULE_functionName, 'function'], + [MySqlParser.RULE_functionNameCreate, 'function'], + [MySqlParser.RULE_columnName, 'column'], + [MySqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case MySqlParser.RULE_databaseName: + case MySqlParser.RULE_tableName: + case MySqlParser.RULE_functionName: + case MySqlParser.RULE_viewName: + case MySqlParser.RULE_columnName: { + result.push(`{existing}${name}`); + break; + } + case MySqlParser.RULE_databaseNameCreate: + case MySqlParser.RULE_tableNameCreate: + case MySqlParser.RULE_functionNameCreate: + case MySqlParser.RULE_viewNameCreate: + case MySqlParser.RULE_columnNameCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join(`{or}`); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/postgresql/index.ts b/src/parser/postgresql/index.ts index 4da31c7b..63820000 100644 --- a/src/parser/postgresql/index.ts +++ b/src/parser/postgresql/index.ts @@ -8,6 +8,8 @@ import { BasicSQL } from '../common/basicSQL'; import { StmtContextType } from '../common/entityCollector'; import { PostgreSqlEntityCollector } from './postgreEntityCollector'; import { PostgreSqlSplitListener } from './postgreSplitListener'; +import { ErrorListener } from '../common/parseErrorListener'; +import { PostgreSqlErrorListener } from './postgreErrorListener'; export { PostgreSqlEntityCollector, PostgreSqlSplitListener }; @@ -41,6 +43,10 @@ export class PostgreSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [PostgreSqlParser.RULE_database_name, 'database'], + [PostgreSqlParser.RULE_database_name_create, 'database'], + [PostgreSqlParser.RULE_table_name, 'table'], + [PostgreSqlParser.RULE_table_name_create, 'table'], + [PostgreSqlParser.RULE_view_name, 'view'], + [PostgreSqlParser.RULE_view_name_create, 'view'], + [PostgreSqlParser.RULE_function_name, 'function'], + [PostgreSqlParser.RULE_function_name_create, 'function'], + [PostgreSqlParser.RULE_column_name, 'column'], + [PostgreSqlParser.RULE_column_name_create, 'column'], + [PostgreSqlParser.RULE_schema_name_create, 'schema'], + [PostgreSqlParser.RULE_schema_name, 'schema'], + [PostgreSqlParser.RULE_procedure_name_create, 'procedure'], + [PostgreSqlParser.RULE_procedure_name, 'procedure'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case PostgreSqlParser.RULE_table_name: + case PostgreSqlParser.RULE_function_name: + case PostgreSqlParser.RULE_schema_name: + case PostgreSqlParser.RULE_view_name: + case PostgreSqlParser.RULE_database_name: + case PostgreSqlParser.RULE_procedure_name: + case PostgreSqlParser.RULE_column_name: { + result.push(`{existing}${name}`); + break; + } + case PostgreSqlParser.RULE_table_name_create: + case PostgreSqlParser.RULE_function_name_create: + case PostgreSqlParser.RULE_schema_name_create: + case PostgreSqlParser.RULE_view_name_create: + case PostgreSqlParser.RULE_database_name_create: + case PostgreSqlParser.RULE_procedure_name_create: + case PostgreSqlParser.RULE_column_name_create: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join('{or}'); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/spark/index.ts b/src/parser/spark/index.ts index afbe5585..1b847fd8 100644 --- a/src/parser/spark/index.ts +++ b/src/parser/spark/index.ts @@ -7,6 +7,8 @@ import { Suggestions, EntityContextType, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { SparkSqlSplitListener } from './sparkSplitListener'; import { SparkEntityCollector } from './sparkEntityCollector'; +import { SparkErrorListener } from './sparkErrorListener'; +import { ErrorListener } from '../common/parseErrorListener'; export { SparkSqlSplitListener, SparkEntityCollector }; @@ -36,6 +38,10 @@ export class SparkSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [SparkSqlParser.RULE_namespaceName, 'namespace'], + [SparkSqlParser.RULE_namespaceNameCreate, 'namespace'], + [SparkSqlParser.RULE_tableName, 'table'], + [SparkSqlParser.RULE_tableNameCreate, 'table'], + [SparkSqlParser.RULE_viewName, 'view'], + [SparkSqlParser.RULE_viewNameCreate, 'view'], + [SparkSqlParser.RULE_functionName, 'function'], + [SparkSqlParser.RULE_functionNameCreate, 'function'], + [SparkSqlParser.RULE_columnName, 'column'], + [SparkSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case SparkSqlParser.RULE_namespaceName: + case SparkSqlParser.RULE_tableName: + case SparkSqlParser.RULE_viewName: + case SparkSqlParser.RULE_functionName: + case SparkSqlParser.RULE_columnName: { + result.push(`{existing}${name}`); + break; + } + case SparkSqlParser.RULE_namespaceNameCreate: + case SparkSqlParser.RULE_tableNameCreate: + case SparkSqlParser.RULE_functionNameCreate: + case SparkSqlParser.RULE_viewNameCreate: + case SparkSqlParser.RULE_columnNameCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join('{or}'); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/src/parser/trino/index.ts b/src/parser/trino/index.ts index 61ff9409..554b0b15 100644 --- a/src/parser/trino/index.ts +++ b/src/parser/trino/index.ts @@ -7,6 +7,8 @@ import { Suggestions, EntityContextType, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { TrinoSqlSplitListener } from './trinoSplitListener'; import { TrinoEntityCollector } from './trinoEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { TrinoErrorListener } from './trinoErrorListener'; export { TrinoSqlSplitListener, TrinoEntityCollector }; @@ -23,6 +25,10 @@ export class TrinoSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [TrinoSqlParser.RULE_catalogName, 'catalog'], + [TrinoSqlParser.RULE_catalogNameCreate, 'catalog'], + [TrinoSqlParser.RULE_tableName, 'table'], + [TrinoSqlParser.RULE_tableNameCreate, 'table'], + [TrinoSqlParser.RULE_viewName, 'view'], + [TrinoSqlParser.RULE_viewNameCreate, 'view'], + [TrinoSqlParser.RULE_schemaName, 'schema'], + [TrinoSqlParser.RULE_schemaNameCreate, 'schema'], + [TrinoSqlParser.RULE_functionName, 'function'], + [TrinoSqlParser.RULE_columnName, 'column'], + [TrinoSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context ?? undefined; + while (currentContext?.parent) { + currentContext = currentContext.parent; + } + + const core = new CodeCompletionCore(parser); + core.preferredRules = this.preferredRules; + const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + if (candidates.rules.size) { + const result: string[] = []; + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case TrinoSqlParser.RULE_catalogName: + case TrinoSqlParser.RULE_schemaName: + case TrinoSqlParser.RULE_tableName: + case TrinoSqlParser.RULE_viewName: + case TrinoSqlParser.RULE_functionName: + case TrinoSqlParser.RULE_columnName: { + result.push(`{existing}${name}`); + break; + } + case TrinoSqlParser.RULE_catalogNameCreate: + case TrinoSqlParser.RULE_tableNameCreate: + case TrinoSqlParser.RULE_schemaNameCreate: + case TrinoSqlParser.RULE_viewNameCreate: + case TrinoSqlParser.RULE_tableNameCreate: { + result.push(`{new}${name}`); + break; + } + } + } + expectedText = result.join('{or}'); + } + if (candidates.tokens.size) { + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; + } + return expectedText; + } +} diff --git a/test/parser/flink/errorListener.test.ts b/test/parser/flink/errorListener.test.ts new file mode 100644 index 00000000..1a82a9b3 --- /dev/null +++ b/test/parser/flink/errorListener.test.ts @@ -0,0 +1,100 @@ +import { FlinkSQL } from 'src/parser/flink'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE VIEW `; +const sql6 = `DROP CATALOG `; + +describe('FlinkSQL validate invalid sql and test msg', () => { + const flink = new FlinkSQL(); + + test('validate random text', () => { + const errors = flink.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = flink.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = flink.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = flink.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = flink.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing column or a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = flink.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `Statement is incomplete, expecting a new view or a keyword` + ); + }); + + test('validate unComplete sql6', () => { + const errors = flink.validate(sql6); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `Statement is incomplete, expecting an existing catalog or a keyword` + ); + }); + + test('validate random text cn', () => { + flink.locale = 'zh_CN'; + const errors = flink.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = flink.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = flink.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = flink.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = flink.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的column或者一个关键字`); + }); +}); diff --git a/test/parser/hive/errorListener.test.ts b/test/parser/hive/errorListener.test.ts new file mode 100644 index 00000000..1bc9b6a1 --- /dev/null +++ b/test/parser/hive/errorListener.test.ts @@ -0,0 +1,91 @@ +import { HiveSQL } from 'src/parser/hive'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE TABLE `; + +describe('HiveSQL validate invalid sql and test msg', () => { + const hive = new HiveSQL(); + + test('validate random text', () => { + const errors = hive.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = hive.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = hive.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = hive.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = hive.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = hive.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `Statement is incomplete, expecting a new table or a keyword` + ); + }); + + test('validate random text cn', () => { + hive.locale = 'zh_CN'; + const errors = hive.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = hive.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = hive.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = hive.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = hive.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个关键字`); + }); +}); diff --git a/test/parser/impala/errorListener.test.ts b/test/parser/impala/errorListener.test.ts new file mode 100644 index 00000000..36a91b40 --- /dev/null +++ b/test/parser/impala/errorListener.test.ts @@ -0,0 +1,91 @@ +import { ImpalaSQL } from 'src/parser/impala'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE VIEW `; + +describe('ImpalaSQL validate invalid sql and test msg', () => { + const impala = new ImpalaSQL(); + + test('validate random text', () => { + const errors = impala.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = impala.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = impala.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = impala.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = impala.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = impala.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `Statement is incomplete, expecting a new view or a keyword` + ); + }); + + test('validate random text cn', () => { + impala.locale = 'zh_CN'; + const errors = impala.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = impala.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = impala.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = impala.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = impala.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个关键字`); + }); +}); diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts new file mode 100644 index 00000000..443666bf --- /dev/null +++ b/test/parser/mysql/errorListener.test.ts @@ -0,0 +1,80 @@ +import { MySQL } from 'src/parser/mysql'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `CREATE DATABASE `; +const sql3 = `SHOW CREATE DATABASE IF NOT EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; + +describe('MySQL validate invalid sql and test msg', () => { + const mysql = new MySQL(); + + test('validate random text', () => { + const errors = mysql.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = mysql.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = mysql.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'Statement is incomplete, expecting a new database or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = mysql.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = mysql.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting an existing column or a keyword` + ); + }); + + test('validate random text cn', () => { + mysql.locale = 'zh_CN'; + const errors = mysql.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = mysql.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = mysql.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toEqual('语句不完整,期望一个新的database或者一个关键字'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = mysql.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = mysql.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个存在的column或者一个关键字`); + }); +}); diff --git a/test/parser/postgresql/errorListener.test.ts b/test/parser/postgresql/errorListener.test.ts new file mode 100644 index 00000000..153f31ab --- /dev/null +++ b/test/parser/postgresql/errorListener.test.ts @@ -0,0 +1,80 @@ +import { PostgreSQL } from 'src/parser/postgresql'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `ALTER EVENT`; +const sql2 = `CREATE FUNCTION `; +const sql3 = `SELECT name, altitude FROM ONLY cities WHERE `; +const sql4 = `DROP PROCEDURE name1 a`; + +describe('PostgreSQL validate invalid sql and test msg', () => { + const pgSQL = new PostgreSQL(); + + test('validate random text', () => { + const errors = pgSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = pgSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = pgSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete, expecting a new function'); + }); + + test('validate unComplete sql3', () => { + const errors = pgSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `Statement is incomplete, expecting an existing function or an existing column or a keyword` + ); + }); + + test('validate unComplete sql4', () => { + const errors = pgSQL.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'a' is not valid at this position, expecting an existing procedure or a keyword` + ); + }); + + test('validate random text cn', () => { + pgSQL.locale = 'zh_CN'; + const errors = pgSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = pgSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = pgSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toEqual('语句不完整,期望一个新的function'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = pgSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `语句不完整,期望一个存在的function或者一个存在的column或者一个关键字` + ); + }); + + test('validate unComplete sql4 cn', () => { + const errors = pgSQL.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'a' 在此位置无效,期望一个存在的procedure或者一个关键字`); + }); +}); diff --git a/test/parser/spark/errorListener.test.ts b/test/parser/spark/errorListener.test.ts new file mode 100644 index 00000000..bc356160 --- /dev/null +++ b/test/parser/spark/errorListener.test.ts @@ -0,0 +1,67 @@ +import { SparkSQL } from 'src/parser/spark'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `ALTER VIEW`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP SCHEMA aaa aaa`; + +describe('SparkSQL validate invalid sql and test msg', () => { + const spark = new SparkSQL(); + + test('validate random text', () => { + const errors = spark.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = spark.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = spark.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'Statement is incomplete, expecting an existing table or an existing view or an existing function or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = spark.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing namespace or a keyword` + ); + }); + + test('validate random text cn', () => { + spark.locale = 'zh_CN'; + const errors = spark.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = spark.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = spark.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个存在的function或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = spark.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的namespace或者一个关键字`); + }); +}); diff --git a/test/parser/trino/errorListener.test.ts b/test/parser/trino/errorListener.test.ts new file mode 100644 index 00000000..c4d174d6 --- /dev/null +++ b/test/parser/trino/errorListener.test.ts @@ -0,0 +1,63 @@ +import { TrinoSQL } from 'src/parser/trino'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `CREATE VIEW `; +const sql3 = `SHOW CREATE TABLE aaa aaa`; + +describe('TrinoSQL validate invalid sql and test msg', () => { + const trinoSQL = new TrinoSQL(); + + test('validate random text', () => { + const errors = trinoSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = trinoSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = trinoSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('Statement is incomplete, expecting a new view'); + }); + + test('validate unComplete sql3', () => { + const errors = trinoSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing table or a keyword` + ); + }); + + test('validate random text cn', () => { + trinoSQL.locale = 'zh_CN'; + const errors = trinoSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = trinoSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = trinoSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toEqual('语句不完整,期望一个新的view'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = trinoSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的table或者一个关键字`); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 2329d6f8..c27a683b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,8 @@ "noImplicitReturns": true, "noImplicitThis": true, "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, "lib": [ "ESNext", "DOM"