From 33b4707a04741c175b9bff34883184a12eabd1ca Mon Sep 17 00:00:00 2001 From: liuxy0551 Date: Tue, 12 May 2026 11:11:33 +0800 Subject: [PATCH 1/2] test(parser): #283 add multi-statement error validation tests for all dialects --- test/parser/flink/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/hive/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/impala/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/mysql/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/postgresql/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/spark/errorListener.test.ts | 35 ++++++++++++++++++++ test/parser/trino/errorListener.test.ts | 35 ++++++++++++++++++++ 7 files changed, 245 insertions(+) diff --git a/test/parser/flink/errorListener.test.ts b/test/parser/flink/errorListener.test.ts index c16a8ef5..2257605c 100644 --- a/test/parser/flink/errorListener.test.ts +++ b/test/parser/flink/errorListener.test.ts @@ -115,3 +115,38 @@ describe('FlinkSQL validate invalid sql and test msg', () => { ); }); }); + +describe('FlinkSQL validate multiple erroneous statements', () => { + const flink = new FlinkSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = flink.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = flink.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = flink.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = flink.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = flink.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/hive/errorListener.test.ts b/test/parser/hive/errorListener.test.ts index baa0cb97..fcfa028b 100644 --- a/test/parser/hive/errorListener.test.ts +++ b/test/parser/hive/errorListener.test.ts @@ -112,3 +112,38 @@ describe('HiveSQL validate invalid sql and test msg', () => { ); }); }); + +describe('HiveSQL validate multiple erroneous statements', () => { + const hive = new HiveSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = hive.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = hive.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = hive.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = hive.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = hive.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/impala/errorListener.test.ts b/test/parser/impala/errorListener.test.ts index 26778f9d..6df0be9e 100644 --- a/test/parser/impala/errorListener.test.ts +++ b/test/parser/impala/errorListener.test.ts @@ -104,3 +104,38 @@ describe('ImpalaSQL validate invalid sql and test msg', () => { expect(errors[0].message).toBe(`'<' 在此位置无效,期望一个存在的column或者一个关键字`); }); }); + +describe('ImpalaSQL validate multiple erroneous statements', () => { + const impala = new ImpalaSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = impala.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = impala.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = impala.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = impala.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = impala.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts index a7a44240..3592e1fb 100644 --- a/test/parser/mysql/errorListener.test.ts +++ b/test/parser/mysql/errorListener.test.ts @@ -95,3 +95,38 @@ describe('MySQL validate invalid sql and test msg', () => { ); }); }); + +describe('MySQL validate multiple erroneous statements', () => { + const mysql = new MySQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = mysql.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = mysql.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = mysql.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = mysql.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = mysql.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/postgresql/errorListener.test.ts b/test/parser/postgresql/errorListener.test.ts index 8d570f4b..1db9b571 100644 --- a/test/parser/postgresql/errorListener.test.ts +++ b/test/parser/postgresql/errorListener.test.ts @@ -87,3 +87,38 @@ describe('PostgreSQL validate invalid sql and test msg', () => { expect(errors[0].message).toBe(`'a' 在此位置无效,期望一个存在的procedure或者一个关键字`); }); }); + +describe('PostgreSQL validate multiple erroneous statements', () => { + const pgSQL = new PostgreSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = pgSQL.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = pgSQL.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = pgSQL.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = pgSQL.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = pgSQL.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/spark/errorListener.test.ts b/test/parser/spark/errorListener.test.ts index e9983a3f..b9ac9470 100644 --- a/test/parser/spark/errorListener.test.ts +++ b/test/parser/spark/errorListener.test.ts @@ -82,3 +82,38 @@ describe('SparkSQL validate invalid sql and test msg', () => { ); }); }); + +describe('SparkSQL validate multiple erroneous statements', () => { + const spark = new SparkSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = spark.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = spark.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = spark.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = spark.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = spark.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); diff --git a/test/parser/trino/errorListener.test.ts b/test/parser/trino/errorListener.test.ts index 65cd0bd5..550b28e2 100644 --- a/test/parser/trino/errorListener.test.ts +++ b/test/parser/trino/errorListener.test.ts @@ -80,3 +80,38 @@ describe('TrinoSQL validate invalid sql and test msg', () => { ); }); }); + +describe('TrinoSQL validate multiple erroneous statements', () => { + const trino = new TrinoSQL(); + + test('validate multiple erroneous statements', () => { + const sql = `SELEC * from table1; SELECT * form table2;`; + const errors = trino.validate(sql); + expect(errors.length).toBe(2); + }); + + test('validate valid + erroneous statements', () => { + const sql = `SELECT * from table1; SELEC * from table2;`; + const errors = trino.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate erroneous + valid statements', () => { + const sql = `SELEC * from table1; SELECT * from table2;`; + const errors = trino.validate(sql); + expect(errors.length).toBe(1); + }); + + test('validate multiple valid statements', () => { + const sql = `SELECT * from table1; SELECT * from table2;`; + const errors = trino.validate(sql); + expect(errors.length).toBe(0); + }); + + test('validate multiline erroneous statement reports correct line', () => { + const sql = `SELECT * from table1;\nSELEC *\n from table2;`; + const errors = trino.validate(sql); + expect(errors.length).toBe(1); + expect(errors[0].startLine).toBe(2); + }); +}); From c42c504022840a940388d71c0d2a3cb5f5d332ad Mon Sep 17 00:00:00 2001 From: liuxy0551 Date: Tue, 12 May 2026 11:17:59 +0800 Subject: [PATCH 2/2] fix(parser): #283 collect errors from all erroneous statements in multi-statement input --- src/parser/common/basicSQL.ts | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index cf96267b..8b234664 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -215,14 +215,92 @@ export abstract class BasicSQL< /** * Validate input string and return syntax errors if exists. + * When input contains multiple statements separated by semicolons + * and the initial parse doesn't capture all errors, each statement + * is validated independently to collect all errors. * @param input source string * @returns syntax errors */ public validate(input: string): ParseError[] { this.parseWithCache(input); + + if (this._parseErrors.length > 0) { + const statements = this.splitStatements(input); + if (statements.length > 1) { + const splitErrors = this.validateStatements(statements); + if (splitErrors.length > this._parseErrors.length) { + this._parseErrors = splitErrors; + } + } + } + return this._parseErrors; } + /** + * Validate each statement fragment independently and collect all errors. + */ + private validateStatements(statements: { text: string; start: number }[]): ParseError[] { + const allErrors: ParseError[] = []; + let lineOffset = 0; + for (const statement of statements) { + if (!statement.text.trim()) { + const newlines = (statement.text.match(/\n/g) || []).length; + lineOffset += newlines; + continue; + } + const parser = this.createParser(statement.text); + const errors: ParseError[] = []; + parser.removeErrorListeners(); + parser.addErrorListener( + this.createErrorListener((error) => { + errors.push({ + startLine: error.startLine + lineOffset, + endLine: error.endLine + lineOffset, + startColumn: error.startColumn, + endColumn: error.endColumn, + message: error.message, + }); + }) + ); + parser.errorHandler = new ErrorStrategy(); + parser.program(); + allErrors.push(...errors); + const newlines = (statement.text.match(/\n/g) || []).length; + lineOffset += newlines; + } + return allErrors; + } + + /** + * Split input into individual statement strings by semicolons. + * Handles semicolons inside quoted strings correctly. + */ + private splitStatements(input: string): { text: string; start: number }[] { + const statements: { text: string; start: number }[] = []; + let inSingleQuote = false; + let inDoubleQuote = false; + let lastSplit = 0; + + for (let i = 0; i < input.length; i++) { + const ch = input[i]; + if (ch === "'" && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + } else if (ch === '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + } else if (ch === ';' && !inSingleQuote && !inDoubleQuote) { + statements.push({ text: input.slice(lastSplit, i + 1), start: lastSplit }); + lastSplit = i + 1; + } + } + + if (lastSplit < input.length) { + statements.push({ text: input.slice(lastSplit), start: lastSplit }); + } + + return statements; + } + /** * Get the input string that has been parsed. */