Skip to content

Commit 0041ed2

Browse files
authored
fix(parser): #283 collect errors from all erroneous statements in multi-statement input (#470)
* test(parser): #283 add multi-statement error validation tests for all dialects * fix(parser): #283 collect errors from all erroneous statements in multi-statement input
1 parent fe15ad7 commit 0041ed2

8 files changed

Lines changed: 323 additions & 0 deletions

File tree

src/parser/common/basicSQL.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,92 @@ export abstract class BasicSQL<
215215

216216
/**
217217
* Validate input string and return syntax errors if exists.
218+
* When input contains multiple statements separated by semicolons
219+
* and the initial parse doesn't capture all errors, each statement
220+
* is validated independently to collect all errors.
218221
* @param input source string
219222
* @returns syntax errors
220223
*/
221224
public validate(input: string): ParseError[] {
222225
this.parseWithCache(input);
226+
227+
if (this._parseErrors.length > 0) {
228+
const statements = this.splitStatements(input);
229+
if (statements.length > 1) {
230+
const splitErrors = this.validateStatements(statements);
231+
if (splitErrors.length > this._parseErrors.length) {
232+
this._parseErrors = splitErrors;
233+
}
234+
}
235+
}
236+
223237
return this._parseErrors;
224238
}
225239

240+
/**
241+
* Validate each statement fragment independently and collect all errors.
242+
*/
243+
private validateStatements(statements: { text: string; start: number }[]): ParseError[] {
244+
const allErrors: ParseError[] = [];
245+
let lineOffset = 0;
246+
for (const statement of statements) {
247+
if (!statement.text.trim()) {
248+
const newlines = (statement.text.match(/\n/g) || []).length;
249+
lineOffset += newlines;
250+
continue;
251+
}
252+
const parser = this.createParser(statement.text);
253+
const errors: ParseError[] = [];
254+
parser.removeErrorListeners();
255+
parser.addErrorListener(
256+
this.createErrorListener((error) => {
257+
errors.push({
258+
startLine: error.startLine + lineOffset,
259+
endLine: error.endLine + lineOffset,
260+
startColumn: error.startColumn,
261+
endColumn: error.endColumn,
262+
message: error.message,
263+
});
264+
})
265+
);
266+
parser.errorHandler = new ErrorStrategy();
267+
parser.program();
268+
allErrors.push(...errors);
269+
const newlines = (statement.text.match(/\n/g) || []).length;
270+
lineOffset += newlines;
271+
}
272+
return allErrors;
273+
}
274+
275+
/**
276+
* Split input into individual statement strings by semicolons.
277+
* Handles semicolons inside quoted strings correctly.
278+
*/
279+
private splitStatements(input: string): { text: string; start: number }[] {
280+
const statements: { text: string; start: number }[] = [];
281+
let inSingleQuote = false;
282+
let inDoubleQuote = false;
283+
let lastSplit = 0;
284+
285+
for (let i = 0; i < input.length; i++) {
286+
const ch = input[i];
287+
if (ch === "'" && !inDoubleQuote) {
288+
inSingleQuote = !inSingleQuote;
289+
} else if (ch === '"' && !inSingleQuote) {
290+
inDoubleQuote = !inDoubleQuote;
291+
} else if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
292+
statements.push({ text: input.slice(lastSplit, i + 1), start: lastSplit });
293+
lastSplit = i + 1;
294+
}
295+
}
296+
297+
if (lastSplit < input.length) {
298+
statements.push({ text: input.slice(lastSplit), start: lastSplit });
299+
}
300+
301+
return statements;
302+
}
303+
226304
/**
227305
* Get the input string that has been parsed.
228306
*/

test/parser/flink/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,38 @@ describe('FlinkSQL validate invalid sql and test msg', () => {
115115
);
116116
});
117117
});
118+
119+
describe('FlinkSQL validate multiple erroneous statements', () => {
120+
const flink = new FlinkSQL();
121+
122+
test('validate multiple erroneous statements', () => {
123+
const sql = `SELEC * from table1; SELECT * form table2;`;
124+
const errors = flink.validate(sql);
125+
expect(errors.length).toBe(2);
126+
});
127+
128+
test('validate valid + erroneous statements', () => {
129+
const sql = `SELECT * from table1; SELEC * from table2;`;
130+
const errors = flink.validate(sql);
131+
expect(errors.length).toBe(1);
132+
});
133+
134+
test('validate erroneous + valid statements', () => {
135+
const sql = `SELEC * from table1; SELECT * from table2;`;
136+
const errors = flink.validate(sql);
137+
expect(errors.length).toBe(1);
138+
});
139+
140+
test('validate multiple valid statements', () => {
141+
const sql = `SELECT * from table1; SELECT * from table2;`;
142+
const errors = flink.validate(sql);
143+
expect(errors.length).toBe(0);
144+
});
145+
146+
test('validate multiline erroneous statement reports correct line', () => {
147+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
148+
const errors = flink.validate(sql);
149+
expect(errors.length).toBe(1);
150+
expect(errors[0].startLine).toBe(2);
151+
});
152+
});

test/parser/hive/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,38 @@ describe('HiveSQL validate invalid sql and test msg', () => {
112112
);
113113
});
114114
});
115+
116+
describe('HiveSQL validate multiple erroneous statements', () => {
117+
const hive = new HiveSQL();
118+
119+
test('validate multiple erroneous statements', () => {
120+
const sql = `SELEC * from table1; SELECT * form table2;`;
121+
const errors = hive.validate(sql);
122+
expect(errors.length).toBe(2);
123+
});
124+
125+
test('validate valid + erroneous statements', () => {
126+
const sql = `SELECT * from table1; SELEC * from table2;`;
127+
const errors = hive.validate(sql);
128+
expect(errors.length).toBe(1);
129+
});
130+
131+
test('validate erroneous + valid statements', () => {
132+
const sql = `SELEC * from table1; SELECT * from table2;`;
133+
const errors = hive.validate(sql);
134+
expect(errors.length).toBe(1);
135+
});
136+
137+
test('validate multiple valid statements', () => {
138+
const sql = `SELECT * from table1; SELECT * from table2;`;
139+
const errors = hive.validate(sql);
140+
expect(errors.length).toBe(0);
141+
});
142+
143+
test('validate multiline erroneous statement reports correct line', () => {
144+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
145+
const errors = hive.validate(sql);
146+
expect(errors.length).toBe(1);
147+
expect(errors[0].startLine).toBe(2);
148+
});
149+
});

test/parser/impala/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,38 @@ describe('ImpalaSQL validate invalid sql and test msg', () => {
104104
expect(errors[0].message).toBe(`'<' 在此位置无效,期望一个存在的column或者一个关键字`);
105105
});
106106
});
107+
108+
describe('ImpalaSQL validate multiple erroneous statements', () => {
109+
const impala = new ImpalaSQL();
110+
111+
test('validate multiple erroneous statements', () => {
112+
const sql = `SELEC * from table1; SELECT * form table2;`;
113+
const errors = impala.validate(sql);
114+
expect(errors.length).toBe(2);
115+
});
116+
117+
test('validate valid + erroneous statements', () => {
118+
const sql = `SELECT * from table1; SELEC * from table2;`;
119+
const errors = impala.validate(sql);
120+
expect(errors.length).toBe(1);
121+
});
122+
123+
test('validate erroneous + valid statements', () => {
124+
const sql = `SELEC * from table1; SELECT * from table2;`;
125+
const errors = impala.validate(sql);
126+
expect(errors.length).toBe(1);
127+
});
128+
129+
test('validate multiple valid statements', () => {
130+
const sql = `SELECT * from table1; SELECT * from table2;`;
131+
const errors = impala.validate(sql);
132+
expect(errors.length).toBe(0);
133+
});
134+
135+
test('validate multiline erroneous statement reports correct line', () => {
136+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
137+
const errors = impala.validate(sql);
138+
expect(errors.length).toBe(1);
139+
expect(errors[0].startLine).toBe(2);
140+
});
141+
});

test/parser/mysql/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,38 @@ describe('MySQL validate invalid sql and test msg', () => {
9595
);
9696
});
9797
});
98+
99+
describe('MySQL validate multiple erroneous statements', () => {
100+
const mysql = new MySQL();
101+
102+
test('validate multiple erroneous statements', () => {
103+
const sql = `SELEC * from table1; SELECT * form table2;`;
104+
const errors = mysql.validate(sql);
105+
expect(errors.length).toBe(2);
106+
});
107+
108+
test('validate valid + erroneous statements', () => {
109+
const sql = `SELECT * from table1; SELEC * from table2;`;
110+
const errors = mysql.validate(sql);
111+
expect(errors.length).toBe(1);
112+
});
113+
114+
test('validate erroneous + valid statements', () => {
115+
const sql = `SELEC * from table1; SELECT * from table2;`;
116+
const errors = mysql.validate(sql);
117+
expect(errors.length).toBe(1);
118+
});
119+
120+
test('validate multiple valid statements', () => {
121+
const sql = `SELECT * from table1; SELECT * from table2;`;
122+
const errors = mysql.validate(sql);
123+
expect(errors.length).toBe(0);
124+
});
125+
126+
test('validate multiline erroneous statement reports correct line', () => {
127+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
128+
const errors = mysql.validate(sql);
129+
expect(errors.length).toBe(1);
130+
expect(errors[0].startLine).toBe(2);
131+
});
132+
});

test/parser/postgresql/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,38 @@ describe('PostgreSQL validate invalid sql and test msg', () => {
8787
expect(errors[0].message).toBe(`'a' 在此位置无效,期望一个存在的procedure或者一个关键字`);
8888
});
8989
});
90+
91+
describe('PostgreSQL validate multiple erroneous statements', () => {
92+
const pgSQL = new PostgreSQL();
93+
94+
test('validate multiple erroneous statements', () => {
95+
const sql = `SELEC * from table1; SELECT * form table2;`;
96+
const errors = pgSQL.validate(sql);
97+
expect(errors.length).toBe(2);
98+
});
99+
100+
test('validate valid + erroneous statements', () => {
101+
const sql = `SELECT * from table1; SELEC * from table2;`;
102+
const errors = pgSQL.validate(sql);
103+
expect(errors.length).toBe(1);
104+
});
105+
106+
test('validate erroneous + valid statements', () => {
107+
const sql = `SELEC * from table1; SELECT * from table2;`;
108+
const errors = pgSQL.validate(sql);
109+
expect(errors.length).toBe(1);
110+
});
111+
112+
test('validate multiple valid statements', () => {
113+
const sql = `SELECT * from table1; SELECT * from table2;`;
114+
const errors = pgSQL.validate(sql);
115+
expect(errors.length).toBe(0);
116+
});
117+
118+
test('validate multiline erroneous statement reports correct line', () => {
119+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
120+
const errors = pgSQL.validate(sql);
121+
expect(errors.length).toBe(1);
122+
expect(errors[0].startLine).toBe(2);
123+
});
124+
});

test/parser/spark/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,38 @@ describe('SparkSQL validate invalid sql and test msg', () => {
8282
);
8383
});
8484
});
85+
86+
describe('SparkSQL validate multiple erroneous statements', () => {
87+
const spark = new SparkSQL();
88+
89+
test('validate multiple erroneous statements', () => {
90+
const sql = `SELEC * from table1; SELECT * form table2;`;
91+
const errors = spark.validate(sql);
92+
expect(errors.length).toBe(2);
93+
});
94+
95+
test('validate valid + erroneous statements', () => {
96+
const sql = `SELECT * from table1; SELEC * from table2;`;
97+
const errors = spark.validate(sql);
98+
expect(errors.length).toBe(1);
99+
});
100+
101+
test('validate erroneous + valid statements', () => {
102+
const sql = `SELEC * from table1; SELECT * from table2;`;
103+
const errors = spark.validate(sql);
104+
expect(errors.length).toBe(1);
105+
});
106+
107+
test('validate multiple valid statements', () => {
108+
const sql = `SELECT * from table1; SELECT * from table2;`;
109+
const errors = spark.validate(sql);
110+
expect(errors.length).toBe(0);
111+
});
112+
113+
test('validate multiline erroneous statement reports correct line', () => {
114+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
115+
const errors = spark.validate(sql);
116+
expect(errors.length).toBe(1);
117+
expect(errors[0].startLine).toBe(2);
118+
});
119+
});

test/parser/trino/errorListener.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,38 @@ describe('TrinoSQL validate invalid sql and test msg', () => {
8080
);
8181
});
8282
});
83+
84+
describe('TrinoSQL validate multiple erroneous statements', () => {
85+
const trino = new TrinoSQL();
86+
87+
test('validate multiple erroneous statements', () => {
88+
const sql = `SELEC * from table1; SELECT * form table2;`;
89+
const errors = trino.validate(sql);
90+
expect(errors.length).toBe(2);
91+
});
92+
93+
test('validate valid + erroneous statements', () => {
94+
const sql = `SELECT * from table1; SELEC * from table2;`;
95+
const errors = trino.validate(sql);
96+
expect(errors.length).toBe(1);
97+
});
98+
99+
test('validate erroneous + valid statements', () => {
100+
const sql = `SELEC * from table1; SELECT * from table2;`;
101+
const errors = trino.validate(sql);
102+
expect(errors.length).toBe(1);
103+
});
104+
105+
test('validate multiple valid statements', () => {
106+
const sql = `SELECT * from table1; SELECT * from table2;`;
107+
const errors = trino.validate(sql);
108+
expect(errors.length).toBe(0);
109+
});
110+
111+
test('validate multiline erroneous statement reports correct line', () => {
112+
const sql = `SELECT * from table1;\nSELEC *\n from table2;`;
113+
const errors = trino.validate(sql);
114+
expect(errors.length).toBe(1);
115+
expect(errors[0].startLine).toBe(2);
116+
});
117+
});

0 commit comments

Comments
 (0)