Skip to content

Commit c42c504

Browse files
committed
fix(parser): #283 collect errors from all erroneous statements in multi-statement input
1 parent 33b4707 commit c42c504

1 file changed

Lines changed: 78 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
*/

0 commit comments

Comments
 (0)