Skip to content

Commit a5a8de8

Browse files
authored
boost expression operators over clause keywords in WHERE context (#15)
1 parent 4519715 commit a5a8de8

File tree

6 files changed

+494
-18
lines changed

6 files changed

+494
-18
lines changed

src/autocomplete/content-assist.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { parser, parse as parseRaw } from "../parser/parser"
33
import { visitor } from "../parser/visitor"
44
import { QuestDBLexer } from "../parser/lexer"
55
import type { Statement } from "../parser/ast"
6-
import { IDENTIFIER_KEYWORD_TOKENS } from "./token-classification"
6+
import {
7+
IDENTIFIER_KEYWORD_TOKENS,
8+
EXPRESSION_OPERATORS,
9+
} from "./token-classification"
710

811
// =============================================================================
912
// Constants
@@ -85,6 +88,8 @@ export interface ContentAssistResult {
8588
* context. Used by the provider to boost tables containing all these columns.
8689
*/
8790
referencedColumns: Set<string>
91+
/** Whether the cursor is inside a WHERE clause expression */
92+
isConditionContext: boolean
8893
}
8994

9095
// =============================================================================
@@ -683,6 +688,7 @@ interface ComputeResult {
683688
nextTokenTypes: TokenType[]
684689
suggestColumns: boolean
685690
suggestTables: boolean
691+
isConditionContext: boolean
686692
}
687693

688694
/**
@@ -767,7 +773,21 @@ function computeSuggestions(tokens: IToken[]): ComputeResult {
767773
}
768774
}
769775

770-
return { nextTokenTypes: result, suggestColumns, suggestTables }
776+
// Check if an expression operator's ruleStack includes "whereClause".
777+
// Must check operators specifically — Chevrotain explores ahead into
778+
// not-yet-started WHERE paths even from JOIN ON positions.
779+
const isConditionContext = effectiveSuggestions.some(
780+
(s) =>
781+
EXPRESSION_OPERATORS.has(s.nextTokenType.name) &&
782+
s.ruleStack.includes("whereClause"),
783+
)
784+
785+
return {
786+
nextTokenTypes: result,
787+
suggestColumns,
788+
suggestTables,
789+
isConditionContext,
790+
}
771791
}
772792

773793
/**
@@ -906,6 +926,7 @@ export function getContentAssist(
906926
suggestColumns: false,
907927
suggestTables: false,
908928
referencedColumns: new Set(),
929+
isConditionContext: false,
909930
}
910931
}
911932
}
@@ -934,11 +955,13 @@ export function getContentAssist(
934955
let nextTokenTypes: TokenType[] = []
935956
let suggestColumns = false
936957
let suggestTables = false
958+
let isConditionContext = false
937959
try {
938960
const computed = computeSuggestions(tokensForAssist)
939961
nextTokenTypes = computed.nextTokenTypes
940962
suggestColumns = computed.suggestColumns
941963
suggestTables = computed.suggestTables
964+
isConditionContext = computed.isConditionContext
942965
} catch (e) {
943966
// If content assist fails, return empty suggestions
944967
// This can happen with malformed input
@@ -1025,6 +1048,7 @@ export function getContentAssist(
10251048
suggestColumns,
10261049
suggestTables,
10271050
referencedColumns,
1051+
isConditionContext,
10281052
}
10291053
}
10301054

src/autocomplete/provider.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,36 @@
2121
import type { IToken } from "chevrotain"
2222
import { getContentAssist } from "./content-assist"
2323
import { buildSuggestions } from "./suggestion-builder"
24-
import { shouldSkipToken } from "./token-classification"
24+
import {
25+
shouldSkipToken,
26+
SKIP_TOKENS,
27+
PUNCTUATION_TOKENS,
28+
EXPRESSION_OPERATORS,
29+
tokenNameToKeyword,
30+
} from "./token-classification"
2531
import type { AutocompleteProvider, SchemaInfo, Suggestion } from "./types"
2632
import { SuggestionKind, SuggestionPriority } from "./types"
2733

34+
const EXPRESSION_OPERATOR_LABELS = new Set(
35+
[...EXPRESSION_OPERATORS].map(tokenNameToKeyword),
36+
)
37+
38+
function isSchemaColumn(
39+
image: string,
40+
tablesInScope: { table: string; alias?: string }[],
41+
schema: SchemaInfo,
42+
): boolean {
43+
const nameLower = image.toLowerCase()
44+
for (const ref of tablesInScope) {
45+
const cols = schema.columns[ref.table.toLowerCase()]
46+
if (cols?.some((c) => c.name.toLowerCase() === nameLower)) return true
47+
}
48+
for (const cols of Object.values(schema.columns)) {
49+
if (cols.some((c) => c.name.toLowerCase() === nameLower)) return true
50+
}
51+
return false
52+
}
53+
2854
const TABLE_NAME_TOKENS = new Set([
2955
"From",
3056
"Join",
@@ -163,6 +189,7 @@ export function createAutocompleteProvider(
163189
suggestColumns,
164190
suggestTables,
165191
referencedColumns,
192+
isConditionContext,
166193
} = getContentAssist(query, cursorOffset)
167194

168195
// Merge CTE columns into the schema so getColumnsInScope() can find them
@@ -226,6 +253,42 @@ export function createAutocompleteProvider(
226253
if (suggestTables) {
227254
rankTableSuggestions(suggestions, referencedColumns, columnIndex)
228255
}
256+
257+
// In WHERE after a column, boost operators over clause keywords.
258+
if (isConditionContext) {
259+
const hasExpressionOperators = nextTokenTypes.some((t) =>
260+
EXPRESSION_OPERATORS.has(t.name),
261+
)
262+
if (hasExpressionOperators) {
263+
const effectiveTokens =
264+
isMidWord && tokensBefore.length > 0
265+
? tokensBefore.slice(0, -1)
266+
: tokensBefore
267+
const lastToken = effectiveTokens[effectiveTokens.length - 1]
268+
const lastTokenName = lastToken?.tokenType?.name
269+
270+
if (
271+
lastTokenName &&
272+
!SKIP_TOKENS.has(lastTokenName) &&
273+
!PUNCTUATION_TOKENS.has(lastTokenName) &&
274+
isSchemaColumn(
275+
lastToken.image,
276+
effectiveTablesInScope,
277+
effectiveSchema,
278+
)
279+
) {
280+
for (const s of suggestions) {
281+
if (s.kind !== SuggestionKind.Keyword) continue
282+
if (s.label === "IN") {
283+
s.priority = SuggestionPriority.High
284+
} else if (!EXPRESSION_OPERATOR_LABELS.has(s.label)) {
285+
s.priority = SuggestionPriority.MediumLow
286+
}
287+
}
288+
}
289+
}
290+
}
291+
229292
return suggestions
230293
}
231294

src/autocomplete/suggestion-builder.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
import {
1818
SKIP_TOKENS,
1919
PUNCTUATION_TOKENS,
20-
EXPRESSION_OPERATORS,
2120
tokenNameToKeyword,
2221
} from "./token-classification"
2322
import { functions } from "../grammar/index"
@@ -170,16 +169,13 @@ export function buildSuggestions(
170169
// All parser keyword tokens are keywords (not functions).
171170
// Functions are suggested separately in the functions loop below.
172171
const kind = SuggestionKind.Keyword
173-
const priority = EXPRESSION_OPERATORS.has(name)
174-
? SuggestionPriority.MediumLow
175-
: SuggestionPriority.Medium
176172

177173
suggestions.push({
178174
label: keyword,
179175
kind,
180176
insertText: keyword,
181177
filterText: keyword.toLowerCase(),
182-
priority,
178+
priority: SuggestionPriority.Medium,
183179
})
184180
}
185181

src/autocomplete/token-classification.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ export const IDENTIFIER_TOKENS = new Set([
3333
export const IDENTIFIER_KEYWORD_TOKENS = IDENTIFIER_KEYWORD_NAMES
3434

3535
/**
36-
* Expression-continuation operators that are valid after any expression but
37-
* should be deprioritized so clause-level keywords (ASC, DESC, LIMIT, etc.)
38-
* appear first in the suggestion list.
36+
* Expression-level operators and keywords (as opposed to clause-level keywords).
3937
*/
4038
export const EXPRESSION_OPERATORS = new Set([
4139
"And",
@@ -47,18 +45,11 @@ export const EXPRESSION_OPERATORS = new Set([
4745
"Like",
4846
"Ilike",
4947
"Within",
50-
// Subquery/set operators
5148
"All",
5249
"Any",
5350
"Some",
54-
// Expression-start keywords that continue an expression context
5551
"Case",
5652
"Cast",
57-
// Query connectors — valid after any complete query but should not
58-
// overshadow clause-level keywords the user is more likely typing.
59-
"Union",
60-
"Except",
61-
"Intersect",
6253
])
6354

6455
/**

0 commit comments

Comments
 (0)