Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/parser/common/basicSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
35 changes: 35 additions & 0 deletions test/parser/flink/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/hive/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/impala/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/mysql/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/postgresql/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/spark/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 35 additions & 0 deletions test/parser/trino/errorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading