Skip to content

Commit 56b6136

Browse files
committed
fix: #239 preserve multi-level qualified name ranges
1 parent b846974 commit 56b6136

2 files changed

Lines changed: 65 additions & 7 deletions

File tree

src/parser/common/basicSQL.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export abstract class BasicSQL<
8989
allTokens: Token[],
9090
caretTokenIndex: number
9191
): Token[] {
92-
// antlr4-c3 may return both entity and alias candidates; use the nearest candidate's start index as boundary
92+
// antlr4-c3 可能同时返回实体和别名候选,使用后续最近候选的起始位置作为边界
9393
const endTokenIndex = Array.from(candidates.rules.values()).reduce(
9494
(nearestStartTokenIndex, candidateRule) => {
9595
if (candidateRule.startTokenIndex <= candidateStartTokenIndex) {
@@ -103,12 +103,34 @@ export abstract class BasicSQL<
103103
.slice(candidateStartTokenIndex, endTokenIndex)
104104
.reverse()
105105
.find((token) => token.channel === Token.DEFAULT_CHANNEL);
106-
// look past hidden tokens to detect dot, preserving dot and following identifier in incomplete qualified names
107-
const rangeEndTokenIndex =
108-
endTokenIndex <= caretTokenIndex &&
109-
(allTokens[endTokenIndex]?.text === '.' || previousVisibleToken?.text === '.')
110-
? endTokenIndex + 1
111-
: endTokenIndex;
106+
const visibleTokenIndexes = allTokens
107+
.slice(endTokenIndex, caretTokenIndex + 1)
108+
.reduce<number[]>((indexes, token, offset) => {
109+
if (token.channel === Token.DEFAULT_CHANNEL) {
110+
indexes.push(endTokenIndex + offset);
111+
}
112+
return indexes;
113+
}, []);
114+
const firstVisibleToken = allTokens[visibleTokenIndexes[0]];
115+
let rangeEndTokenIndex = endTokenIndex;
116+
117+
// 候选边界可能落在多级限定名中间,需要沿标识符与点号链继续扩展
118+
if (previousVisibleToken?.text === '.' || firstVisibleToken?.text === '.') {
119+
let visibleTokenOffset = 0;
120+
if (previousVisibleToken?.text === '.' && firstVisibleToken) {
121+
rangeEndTokenIndex = visibleTokenIndexes[visibleTokenOffset] + 1;
122+
visibleTokenOffset += 1;
123+
}
124+
125+
while (allTokens[visibleTokenIndexes[visibleTokenOffset]]?.text === '.') {
126+
rangeEndTokenIndex = visibleTokenIndexes[visibleTokenOffset] + 1;
127+
visibleTokenOffset += 1;
128+
if (visibleTokenOffset < visibleTokenIndexes.length) {
129+
rangeEndTokenIndex = visibleTokenIndexes[visibleTokenOffset] + 1;
130+
visibleTokenOffset += 1;
131+
}
132+
}
133+
}
112134

113135
return allTokens
114136
.slice(candidateStartTokenIndex, rangeEndTokenIndex)

test/parser/syntaxSuggestionWordRange.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
SparkSQL,
99
TrinoSQL,
1010
} from 'src/index';
11+
import { CandidatesCollection } from 'antlr4-c3';
12+
import { Token } from 'antlr4ng';
1113
import { EntityContextType } from 'src/parser/common/types';
1214

1315
type SuggestionParser = Pick<MySQL, 'getSuggestionAtCaretPosition'>;
@@ -23,6 +25,22 @@ const parserFactories: Array<[string, () => SuggestionParser]> = [
2325
['GenericSQL', () => new GenericSQL()],
2426
];
2527

28+
class TestableSparkSQL extends SparkSQL {
29+
public getCandidateTokenRangesForTest(
30+
candidates: CandidatesCollection,
31+
candidateStartTokenIndex: number,
32+
allTokens: Token[],
33+
caretTokenIndex: number
34+
): Token[] {
35+
return this.getCandidateTokenRanges(
36+
candidates,
37+
candidateStartTokenIndex,
38+
allTokens,
39+
caretTokenIndex
40+
);
41+
}
42+
}
43+
2644
const scenarios = [
2745
{
2846
name: 'exclude trailing whitespace from table word ranges',
@@ -93,3 +111,21 @@ test('SparkSQL preserves a qualified table name separated by a comment', () => {
93111
'table',
94112
]);
95113
});
114+
115+
test('preserves a multi-level qualified table name when another candidate starts in the middle', () => {
116+
const parser = new TestableSparkSQL();
117+
const allTokens = parser.getAllTokens('catalog.schema.table');
118+
const candidates = new CandidatesCollection();
119+
candidates.rules.set(0, { startTokenIndex: 0, ruleList: [] });
120+
candidates.rules.set(1, { startTokenIndex: 2, ruleList: [] });
121+
122+
const wordRanges = parser.getCandidateTokenRangesForTest(candidates, 0, allTokens, 4);
123+
124+
expect(wordRanges.map((wordRange) => wordRange.text)).toEqual([
125+
'catalog',
126+
'.',
127+
'schema',
128+
'.',
129+
'table',
130+
]);
131+
});

0 commit comments

Comments
 (0)