Skip to content

Commit 527152d

Browse files
emrberkclaude
andauthored
improve autocomplete: compound JOIN suggestions, fix table ranking, remove unsupported joins (#13)
- Suggest compound JOIN keywords (e.g., LEFT JOIN, ASOF JOIN) instead of bare prefixes like LEFT, ASOF that are incomplete on their own - Fix table suggestion ranking: full-match tables now get Medium priority (below columns at High) instead of being interleaved with columns - Extract selectBody rule from selectStatement so WITH clauses do not suggest DECLARE/WITH after CTE definitions - Remove RIGHT JOIN and FULL JOIN from grammar (unsupported by QuestDB) - Update tests for compound join suggestions and grammar changes Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c5ed89 commit 527152d

File tree

10 files changed

+218
-100
lines changed

10 files changed

+218
-100
lines changed

src/autocomplete/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ function rankTableSuggestions(
9696
if (score === undefined) continue
9797
s.priority =
9898
score === referencedColumns.size
99-
? SuggestionPriority.High // full match
100-
: SuggestionPriority.Medium // partial match
99+
? SuggestionPriority.Medium // full match — below columns (High)
100+
: SuggestionPriority.MediumLow // partial match
101101
}
102102
}
103103

src/autocomplete/suggestion-builder.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ function getAllColumns(schema: SchemaInfo): ColumnWithTable[] {
7575
return columns
7676
}
7777

78+
/**
79+
* Join prefix tokens → compound keyword.
80+
* When "Join" is among the valid next tokens, these prefixes are combined
81+
* into compound suggestions (e.g., "Left" → "LEFT JOIN") instead of
82+
* suggesting bare "LEFT" which is incomplete on its own.
83+
*/
84+
const JOIN_COMPOUND_MAP = new Map<string, string>([
85+
["Left", "LEFT JOIN"],
86+
["Inner", "INNER JOIN"],
87+
["Cross", "CROSS JOIN"],
88+
["Asof", "ASOF JOIN"],
89+
["Lt", "LT JOIN"],
90+
["Splice", "SPLICE JOIN"],
91+
["Window", "WINDOW JOIN"],
92+
["Horizon", "HORIZON JOIN"],
93+
["Outer", "OUTER JOIN"],
94+
])
95+
7896
/**
7997
* Build suggestions from parser's nextTokenTypes
8098
*
@@ -100,6 +118,10 @@ export function buildSuggestions(
100118
const includeTables = options?.includeTables ?? true
101119
const isMidWord = options?.isMidWord ?? false
102120

121+
// Detect join context: when "Join" is a valid next token, join prefix
122+
// keywords (LEFT, RIGHT, ASOF, etc.) should be suggested as compounds.
123+
const isJoinContext = tokenTypes.some((t) => t.name === "Join")
124+
103125
// Process each token type from the parser
104126
for (const tokenType of tokenTypes) {
105127
const name = tokenType.name
@@ -120,6 +142,22 @@ export function buildSuggestions(
120142
continue
121143
}
122144

145+
// In join context, combine join prefix tokens into compound keywords
146+
// (e.g., "Left" → "LEFT JOIN") instead of suggesting bare "LEFT".
147+
if (isJoinContext && JOIN_COMPOUND_MAP.has(name)) {
148+
const compound = JOIN_COMPOUND_MAP.get(name)!
149+
if (seenKeywords.has(compound)) continue
150+
seenKeywords.add(compound)
151+
suggestions.push({
152+
label: compound,
153+
kind: SuggestionKind.Keyword,
154+
insertText: compound,
155+
filterText: compound.toLowerCase(),
156+
priority: SuggestionPriority.Medium,
157+
})
158+
continue
159+
}
160+
123161
// Convert token name to keyword display string
124162
const keyword = tokenNameToKeyword(name)
125163

src/parser/ast.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,6 @@ export interface JoinClause extends AstNode {
833833
joinType?:
834834
| "inner"
835835
| "left"
836-
| "right"
837-
| "full"
838836
| "cross"
839837
| "asof"
840838
| "lt"

src/parser/cst-types.d.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export type WithStatementCstChildren = {
5959
withClause: WithClauseCstNode[];
6060
insertStatement?: InsertStatementCstNode[];
6161
updateStatement?: UpdateStatementCstNode[];
62-
selectStatement?: SelectStatementCstNode[];
62+
selectBody?: SelectBodyCstNode[];
6363
};
6464

6565
export interface SelectStatementCstNode extends CstNode {
@@ -70,6 +70,15 @@ export interface SelectStatementCstNode extends CstNode {
7070
export type SelectStatementCstChildren = {
7171
declareClause?: DeclareClauseCstNode[];
7272
withClause?: WithClauseCstNode[];
73+
selectBody: SelectBodyCstNode[];
74+
};
75+
76+
export interface SelectBodyCstNode extends CstNode {
77+
name: "selectBody";
78+
children: SelectBodyCstChildren;
79+
}
80+
81+
export type SelectBodyCstChildren = {
7382
simpleSelect: SimpleSelectCstNode[];
7483
setOperation?: SetOperationCstNode[];
7584
};
@@ -355,8 +364,6 @@ export interface StandardJoinCstNode extends CstNode {
355364

356365
export type StandardJoinCstChildren = {
357366
Left?: IToken[];
358-
Right?: IToken[];
359-
Full?: IToken[];
360367
Outer?: IToken[];
361368
Inner?: IToken[];
362369
Cross?: IToken[];
@@ -2437,6 +2444,7 @@ export interface ICstNodeVisitor<IN, OUT> extends ICstVisitor<IN, OUT> {
24372444
statement(children: StatementCstChildren, param?: IN): OUT;
24382445
withStatement(children: WithStatementCstChildren, param?: IN): OUT;
24392446
selectStatement(children: SelectStatementCstChildren, param?: IN): OUT;
2447+
selectBody(children: SelectBodyCstChildren, param?: IN): OUT;
24402448
withClause(children: WithClauseCstChildren, param?: IN): OUT;
24412449
cteDefinition(children: CteDefinitionCstChildren, param?: IN): OUT;
24422450
simpleSelect(children: SimpleSelectCstChildren, param?: IN): OUT;

src/parser/parser.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,8 @@ class QuestDBParser extends CstParser {
531531
ALT: () => this.SUBRULE(this.updateStatement),
532532
},
533533
{
534-
// SELECT: delegate to selectStatement (its optional declareClause/
535-
// withClause simply won't match since WITH was already consumed)
536-
ALT: () => this.SUBRULE(this.selectStatement),
534+
// SELECT after WITH: no DECLARE/WITH prefixes allowed here
535+
ALT: () => this.SUBRULE(this.selectBody),
537536
},
538537
])
539538
})
@@ -545,6 +544,10 @@ class QuestDBParser extends CstParser {
545544
private selectStatement = this.RULE("selectStatement", () => {
546545
this.OPTION(() => this.SUBRULE(this.declareClause))
547546
this.OPTION2(() => this.SUBRULE(this.withClause))
547+
this.SUBRULE(this.selectBody)
548+
})
549+
550+
private selectBody = this.RULE("selectBody", () => {
548551
this.SUBRULE(this.simpleSelect)
549552
this.MANY(() => {
550553
this.SUBRULE(this.setOperation)
@@ -948,17 +951,13 @@ class QuestDBParser extends CstParser {
948951
])
949952
})
950953

951-
// Standard joins: (INNER | LEFT [OUTER] | RIGHT [OUTER] | FULL [OUTER] | CROSS)? JOIN + ON
954+
// Standard joins: (INNER | LEFT [OUTER] | CROSS)? JOIN + ON
952955
private standardJoin = this.RULE("standardJoin", () => {
953956
this.OPTION(() => {
954957
this.OR([
955958
{
956959
ALT: () => {
957-
this.OR1([
958-
{ ALT: () => this.CONSUME(Left) },
959-
{ ALT: () => this.CONSUME(Right) },
960-
{ ALT: () => this.CONSUME(Full) },
961-
])
960+
this.CONSUME(Left)
962961
this.OPTION1(() => this.CONSUME(Outer))
963962
},
964963
},

src/parser/visitor.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import type {
130130
SampleByClauseCstChildren,
131131
SelectItemCstChildren,
132132
SelectListCstChildren,
133+
SelectBodyCstChildren,
133134
SelectStatementCstChildren,
134135
SetClauseCstChildren,
135136
SetExpressionCstChildren,
@@ -332,8 +333,8 @@ class QuestDBVisitor extends BaseVisitor {
332333
inner = this.visit(ctx.insertStatement) as AST.InsertStatement
333334
} else if (ctx.updateStatement) {
334335
inner = this.visit(ctx.updateStatement) as AST.UpdateStatement
335-
} else if (ctx.selectStatement) {
336-
inner = this.visit(ctx.selectStatement) as AST.SelectStatement
336+
} else if (ctx.selectBody) {
337+
inner = this.visit(ctx.selectBody) as AST.SelectStatement
337338
} else {
338339
throw new Error("withStatement: expected insert, update, or select")
339340
}
@@ -347,7 +348,7 @@ class QuestDBVisitor extends BaseVisitor {
347348
// ==========================================================================
348349

349350
selectStatement(ctx: SelectStatementCstChildren): AST.SelectStatement {
350-
const result = this.visit(ctx.simpleSelect) as AST.SelectStatement
351+
const result = this.visit(ctx.selectBody) as AST.SelectStatement
351352

352353
if (ctx.declareClause) {
353354
result.declare = this.visit(ctx.declareClause) as AST.DeclareClause
@@ -357,6 +358,12 @@ class QuestDBVisitor extends BaseVisitor {
357358
result.with = this.visit(ctx.withClause) as AST.CTE[]
358359
}
359360

361+
return result
362+
}
363+
364+
selectBody(ctx: SelectBodyCstChildren): AST.SelectStatement {
365+
const result = this.visit(ctx.simpleSelect) as AST.SelectStatement
366+
360367
if (ctx.setOperation && ctx.setOperation.length > 0) {
361368
result.setOperations = ctx.setOperation.map(
362369
(op: CstNode) => this.visit(op) as AST.SetOperation,
@@ -775,8 +782,6 @@ class QuestDBVisitor extends BaseVisitor {
775782
}
776783
if (ctx.Inner) result.joinType = "inner"
777784
else if (ctx.Left) result.joinType = "left"
778-
else if (ctx.Right) result.joinType = "right"
779-
else if (ctx.Full) result.joinType = "full"
780785
else if (ctx.Cross) result.joinType = "cross"
781786
if (ctx.Outer) result.outer = true
782787
if (ctx.expression) {

0 commit comments

Comments
 (0)