Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/autocomplete/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ function rankTableSuggestions(
if (score === undefined) continue
s.priority =
score === referencedColumns.size
? SuggestionPriority.High // full match
: SuggestionPriority.Medium // partial match
? SuggestionPriority.Medium // full match — below columns (High)
: SuggestionPriority.MediumLow // partial match
}
}

Expand Down
38 changes: 38 additions & 0 deletions src/autocomplete/suggestion-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ function getAllColumns(schema: SchemaInfo): ColumnWithTable[] {
return columns
}

/**
* Join prefix tokens → compound keyword.
* When "Join" is among the valid next tokens, these prefixes are combined
* into compound suggestions (e.g., "Left" → "LEFT JOIN") instead of
* suggesting bare "LEFT" which is incomplete on its own.
*/
const JOIN_COMPOUND_MAP = new Map<string, string>([
["Left", "LEFT JOIN"],
["Inner", "INNER JOIN"],
["Cross", "CROSS JOIN"],
["Asof", "ASOF JOIN"],
["Lt", "LT JOIN"],
["Splice", "SPLICE JOIN"],
["Window", "WINDOW JOIN"],
["Horizon", "HORIZON JOIN"],
["Outer", "OUTER JOIN"],
])

/**
* Build suggestions from parser's nextTokenTypes
*
Expand All @@ -100,6 +118,10 @@ export function buildSuggestions(
const includeTables = options?.includeTables ?? true
const isMidWord = options?.isMidWord ?? false

// Detect join context: when "Join" is a valid next token, join prefix
// keywords (LEFT, RIGHT, ASOF, etc.) should be suggested as compounds.
const isJoinContext = tokenTypes.some((t) => t.name === "Join")

// Process each token type from the parser
for (const tokenType of tokenTypes) {
const name = tokenType.name
Expand All @@ -120,6 +142,22 @@ export function buildSuggestions(
continue
}

// In join context, combine join prefix tokens into compound keywords
// (e.g., "Left" → "LEFT JOIN") instead of suggesting bare "LEFT".
if (isJoinContext && JOIN_COMPOUND_MAP.has(name)) {
const compound = JOIN_COMPOUND_MAP.get(name)!
if (seenKeywords.has(compound)) continue
seenKeywords.add(compound)
suggestions.push({
label: compound,
kind: SuggestionKind.Keyword,
insertText: compound,
filterText: compound.toLowerCase(),
priority: SuggestionPriority.Medium,
})
continue
}

// Convert token name to keyword display string
const keyword = tokenNameToKeyword(name)

Expand Down
2 changes: 0 additions & 2 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,6 @@ export interface JoinClause extends AstNode {
joinType?:
| "inner"
| "left"
| "right"
| "full"
| "cross"
| "asof"
| "lt"
Expand Down
14 changes: 11 additions & 3 deletions src/parser/cst-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type WithStatementCstChildren = {
withClause: WithClauseCstNode[];
insertStatement?: InsertStatementCstNode[];
updateStatement?: UpdateStatementCstNode[];
selectStatement?: SelectStatementCstNode[];
selectBody?: SelectBodyCstNode[];
};

export interface SelectStatementCstNode extends CstNode {
Expand All @@ -70,6 +70,15 @@ export interface SelectStatementCstNode extends CstNode {
export type SelectStatementCstChildren = {
declareClause?: DeclareClauseCstNode[];
withClause?: WithClauseCstNode[];
selectBody: SelectBodyCstNode[];
};

export interface SelectBodyCstNode extends CstNode {
name: "selectBody";
children: SelectBodyCstChildren;
}

export type SelectBodyCstChildren = {
simpleSelect: SimpleSelectCstNode[];
setOperation?: SetOperationCstNode[];
};
Expand Down Expand Up @@ -355,8 +364,6 @@ export interface StandardJoinCstNode extends CstNode {

export type StandardJoinCstChildren = {
Left?: IToken[];
Right?: IToken[];
Full?: IToken[];
Outer?: IToken[];
Inner?: IToken[];
Cross?: IToken[];
Expand Down Expand Up @@ -2437,6 +2444,7 @@ export interface ICstNodeVisitor<IN, OUT> extends ICstVisitor<IN, OUT> {
statement(children: StatementCstChildren, param?: IN): OUT;
withStatement(children: WithStatementCstChildren, param?: IN): OUT;
selectStatement(children: SelectStatementCstChildren, param?: IN): OUT;
selectBody(children: SelectBodyCstChildren, param?: IN): OUT;
withClause(children: WithClauseCstChildren, param?: IN): OUT;
cteDefinition(children: CteDefinitionCstChildren, param?: IN): OUT;
simpleSelect(children: SimpleSelectCstChildren, param?: IN): OUT;
Expand Down
17 changes: 8 additions & 9 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,8 @@ class QuestDBParser extends CstParser {
ALT: () => this.SUBRULE(this.updateStatement),
},
{
// SELECT: delegate to selectStatement (its optional declareClause/
// withClause simply won't match since WITH was already consumed)
ALT: () => this.SUBRULE(this.selectStatement),
// SELECT after WITH: no DECLARE/WITH prefixes allowed here
ALT: () => this.SUBRULE(this.selectBody),
},
])
})
Expand All @@ -545,6 +544,10 @@ class QuestDBParser extends CstParser {
private selectStatement = this.RULE("selectStatement", () => {
this.OPTION(() => this.SUBRULE(this.declareClause))
this.OPTION2(() => this.SUBRULE(this.withClause))
this.SUBRULE(this.selectBody)
})

private selectBody = this.RULE("selectBody", () => {
this.SUBRULE(this.simpleSelect)
this.MANY(() => {
this.SUBRULE(this.setOperation)
Expand Down Expand Up @@ -948,17 +951,13 @@ class QuestDBParser extends CstParser {
])
})

// Standard joins: (INNER | LEFT [OUTER] | RIGHT [OUTER] | FULL [OUTER] | CROSS)? JOIN + ON
// Standard joins: (INNER | LEFT [OUTER] | CROSS)? JOIN + ON
private standardJoin = this.RULE("standardJoin", () => {
this.OPTION(() => {
this.OR([
{
ALT: () => {
this.OR1([
{ ALT: () => this.CONSUME(Left) },
{ ALT: () => this.CONSUME(Right) },
{ ALT: () => this.CONSUME(Full) },
])
this.CONSUME(Left)
this.OPTION1(() => this.CONSUME(Outer))
},
},
Expand Down
15 changes: 10 additions & 5 deletions src/parser/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import type {
SampleByClauseCstChildren,
SelectItemCstChildren,
SelectListCstChildren,
SelectBodyCstChildren,
SelectStatementCstChildren,
SetClauseCstChildren,
SetExpressionCstChildren,
Expand Down Expand Up @@ -332,8 +333,8 @@ class QuestDBVisitor extends BaseVisitor {
inner = this.visit(ctx.insertStatement) as AST.InsertStatement
} else if (ctx.updateStatement) {
inner = this.visit(ctx.updateStatement) as AST.UpdateStatement
} else if (ctx.selectStatement) {
inner = this.visit(ctx.selectStatement) as AST.SelectStatement
} else if (ctx.selectBody) {
inner = this.visit(ctx.selectBody) as AST.SelectStatement
} else {
throw new Error("withStatement: expected insert, update, or select")
}
Expand All @@ -347,7 +348,7 @@ class QuestDBVisitor extends BaseVisitor {
// ==========================================================================

selectStatement(ctx: SelectStatementCstChildren): AST.SelectStatement {
const result = this.visit(ctx.simpleSelect) as AST.SelectStatement
const result = this.visit(ctx.selectBody) as AST.SelectStatement

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

return result
}

selectBody(ctx: SelectBodyCstChildren): AST.SelectStatement {
const result = this.visit(ctx.simpleSelect) as AST.SelectStatement

if (ctx.setOperation && ctx.setOperation.length > 0) {
result.setOperations = ctx.setOperation.map(
(op: CstNode) => this.visit(op) as AST.SetOperation,
Expand Down Expand Up @@ -775,8 +782,6 @@ class QuestDBVisitor extends BaseVisitor {
}
if (ctx.Inner) result.joinType = "inner"
else if (ctx.Left) result.joinType = "left"
else if (ctx.Right) result.joinType = "right"
else if (ctx.Full) result.joinType = "full"
else if (ctx.Cross) result.joinType = "cross"
if (ctx.Outer) result.outer = true
if (ctx.expression) {
Expand Down
Loading
Loading