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
32 changes: 32 additions & 0 deletions src/parser/common/basicSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,38 @@ export abstract class BasicSQL<
caretTokenIndex: number
): Suggestions<Token>;

protected getCandidateTokenRanges(
candidates: CandidatesCollection,
candidateStartTokenIndex: number,
allTokens: Token[],
caretTokenIndex: number
): Token[] {
// antlr4-c3 may return both entity and alias candidates; use the nearest candidate's start index as boundary
const endTokenIndex = Array.from(candidates.rules.values()).reduce(
(nearestStartTokenIndex, candidateRule) => {
if (candidateRule.startTokenIndex <= candidateStartTokenIndex) {
return nearestStartTokenIndex;
}
return Math.min(nearestStartTokenIndex, candidateRule.startTokenIndex);
},
caretTokenIndex + 1
);
const previousVisibleToken = allTokens
.slice(candidateStartTokenIndex, endTokenIndex)
.reverse()
.find((token) => token.channel === Token.DEFAULT_CHANNEL);
// look past hidden tokens to detect dot, preserving dot and following identifier in incomplete qualified names
const rangeEndTokenIndex =
endTokenIndex <= caretTokenIndex &&
(allTokens[endTokenIndex]?.text === '.' || previousVisibleToken?.text === '.')
? endTokenIndex + 1
: endTokenIndex;
Comment on lines +107 to +111

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 只够覆盖单层 db.table。若出现 db.schema.table 且被下一个候选截断在 schema,则 table 会被丢掉。这是未覆盖的边界。看是补一个多层限定名用例确认行为还是说在注释里明确"当前仅支持单层保留"。


return allTokens
.slice(candidateStartTokenIndex, rangeEndTokenIndex)
.filter((token) => token.channel === Token.DEFAULT_CHANNEL);
}

/**
* Get a new splitListener instance.
*/
Expand Down
7 changes: 6 additions & 1 deletion src/parser/flink/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ export class FlinkSQL extends BasicSQL<FlinkSqlLexer, ProgramContext, FlinkSqlPa

for (let candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/generic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ export class GenericSQL extends BasicSQL<GenericSqlLexer, ProgramContext, Generi

for (const candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/hive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ export class HiveSQL extends BasicSQL<HiveSqlLexer, ProgramContext, HiveSqlParse
const keywords: string[] = [];
for (let candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/impala/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ export class ImpalaSQL extends BasicSQL<ImpalaSqlLexer, ProgramContext, ImpalaSq
const keywords: string[] = [];
for (let candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/mysql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ export class MySQL extends BasicSQL<MySqlLexer, ProgramContext, MySqlParser> {

for (const candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/postgresql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export class PostgreSQL extends BasicSQL<PostgreSqlLexer, ProgramContext, Postgr
const keywords: string[] = [];
for (let candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/spark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ export class SparkSQL extends BasicSQL<SparkSqlLexer, ProgramContext, SparkSqlPa

for (const candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
7 changes: 6 additions & 1 deletion src/parser/trino/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ export class TrinoSQL extends BasicSQL<TrinoSqlLexer, ProgramContext, TrinoSqlPa

for (let candidate of candidates.rules) {
const [ruleType, candidateRule] = candidate;
const tokenRanges = allTokens.slice(candidateRule.startTokenIndex, caretTokenIndex + 1);
const tokenRanges = this.getCandidateTokenRanges(
candidates,
candidateRule.startTokenIndex,
allTokens,
caretTokenIndex
);

let syntaxContextType: EntityContextType | StmtContextType | undefined = void 0;
switch (ruleType) {
Expand Down
95 changes: 95 additions & 0 deletions test/parser/syntaxSuggestionWordRange.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
FlinkSQL,
GenericSQL,
HiveSQL,
ImpalaSQL,
MySQL,
PostgreSQL,
SparkSQL,
TrinoSQL,
} from 'src/index';
import { EntityContextType } from 'src/parser/common/types';

type SuggestionParser = Pick<MySQL, 'getSuggestionAtCaretPosition'>;

const parserFactories: Array<[string, () => SuggestionParser]> = [
['MySQL', () => new MySQL()],
['FlinkSQL', () => new FlinkSQL()],
['SparkSQL', () => new SparkSQL()],
['HiveSQL', () => new HiveSQL()],
['PostgreSQL', () => new PostgreSQL()],
['TrinoSQL', () => new TrinoSQL()],
['ImpalaSQL', () => new ImpalaSQL()],
['GenericSQL', () => new GenericSQL()],
];

const scenarios = [
{
name: 'exclude trailing whitespace from table word ranges',
sql: 'SELECT * FROM current_catalog_schema1 ',
expected: ['current_catalog_schema1'],
},
{
name: 'exclude AS from table word ranges',
sql: 'SELECT * FROM current_catalog_schema1 as',
expected: ['current_catalog_schema1'],
},
{
name: 'exclude alias from table word ranges',
sql: 'SELECT * FROM current_catalog_schema1 alias',
expected: ['current_catalog_schema1'],
},
{
name: 'preserve an incomplete qualified table name',
sql: 'SELECT * FROM db.',
expected: ['db', '.'],
},
];

describe.each(parserFactories)('%s syntax suggestion word ranges', (_name, createParser) => {
test.each(scenarios)('$name', ({ sql, expected }) => {
const tableSuggestion = createParser()
.getSuggestionAtCaretPosition(sql, {
lineNumber: 1,
column: sql.length + 1,
})
?.syntax.find((suggestion) => suggestion.syntaxContextType === EntityContextType.TABLE);

expect(tableSuggestion).toBeDefined();
expect(tableSuggestion?.wordRanges.map((wordRange) => wordRange.text)).toEqual(expected);
});
});

test('SparkSQL preserves a qualified table name separated by hidden tokens', () => {
const sql = 'SELECT * FROM db. table';
const tableSuggestion = new SparkSQL()
.getSuggestionAtCaretPosition(sql, {
lineNumber: 1,
column: sql.length + 1,
})
?.syntax.find((suggestion) => suggestion.syntaxContextType === EntityContextType.TABLE);

expect(tableSuggestion).toBeDefined();
expect(tableSuggestion?.wordRanges.map((wordRange) => wordRange.text)).toEqual([
'db',
'.',
'table',
]);
});

test('SparkSQL preserves a qualified table name separated by a comment', () => {
const sql = 'SELECT * FROM db./* comment */table';
const tableSuggestion = new SparkSQL()
.getSuggestionAtCaretPosition(sql, {
lineNumber: 1,
column: sql.length + 1,
})
?.syntax.find((suggestion) => suggestion.syntaxContextType === EntityContextType.TABLE);

expect(tableSuggestion).toBeDefined();
expect(tableSuggestion?.wordRanges.map((wordRange) => wordRange.text)).toEqual([
'db',
'.',
'table',
]);
});
Loading