Skip to content

Commit 8c5964d

Browse files
committed
improve table inference, remove redundancy in token classification
1 parent f13ceb9 commit 8c5964d

6 files changed

Lines changed: 670 additions & 704 deletions

File tree

src/autocomplete/content-assist.ts

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ILexingError, IToken, TokenType } from "chevrotain"
22
import { parser } from "../parser/parser"
33
import { QuestDBLexer } from "../parser/lexer"
44
import { parseToAst } from "../index"
5+
import { IDENTIFIER_KEYWORD_TOKENS } from "./token-classification"
56

67
// =============================================================================
78
// Types
@@ -159,36 +160,65 @@ function extractTables(fullSql: string, tokens: IToken[]): TableRef[] {
159160
}
160161

161162
// Fallback: extract from tokens by looking for table name patterns
162-
// Look for Identifier tokens that follow FROM or JOIN tokens
163+
// Handles malformed input where full AST extraction is not available.
163164
const tables: TableRef[] = []
164-
for (let i = 0; i < tokens.length - 1; i++) {
165-
const tokenName = tokens[i].tokenType.name
166-
const nextToken = tokens[i + 1]
167-
168-
if (
169-
(tokenName === "From" ||
170-
tokenName === "Join" ||
171-
tokenName === "AsofJoin" ||
172-
tokenName === "SpliceJoin" ||
173-
tokenName === "LtJoin" ||
174-
tokenName === "CrossJoin" ||
175-
tokenName === "Update" ||
176-
tokenName === "Into") &&
177-
nextToken.tokenType.name === "Identifier"
165+
const TABLE_PREFIX_TOKENS = new Set(["From", "Join", "Update", "Into"])
166+
const isIdentifierLike = (token: IToken | undefined): token is IToken =>
167+
!!token &&
168+
(token.tokenType.name === "Identifier" ||
169+
token.tokenType.name === "QuotedIdentifier" ||
170+
IDENTIFIER_KEYWORD_TOKENS.has(token.tokenType.name))
171+
172+
const tokenToNamePart = (token: IToken): string => {
173+
if (token.tokenType.name === "QuotedIdentifier") {
174+
return token.image.slice(1, -1)
175+
}
176+
return token.image
177+
}
178+
179+
const readQualifiedName = (
180+
startIndex: number,
181+
): { name: string; nextIndex: number } | null => {
182+
const first = tokens[startIndex]
183+
if (!isIdentifierLike(first)) return null
184+
185+
const parts = [tokenToNamePart(first)]
186+
let i = startIndex + 1
187+
188+
while (
189+
i + 1 < tokens.length &&
190+
tokens[i].tokenType.name === "Dot" &&
191+
isIdentifierLike(tokens[i + 1])
178192
) {
179-
const tableName = nextToken.image
180-
// Check for alias (Identifier following the table name)
181-
if (i + 2 < tokens.length) {
182-
const maybeAlias = tokens[i + 2]
183-
if (maybeAlias.tokenType.name === "Identifier") {
184-
tables.push({ table: tableName, alias: maybeAlias.image })
185-
} else {
186-
tables.push({ table: tableName })
187-
}
188-
} else {
189-
tables.push({ table: tableName })
190-
}
193+
parts.push(tokenToNamePart(tokens[i + 1]))
194+
i += 2
191195
}
196+
197+
return { name: parts.join("."), nextIndex: i }
198+
}
199+
200+
for (let i = 0; i < tokens.length; i++) {
201+
if (!TABLE_PREFIX_TOKENS.has(tokens[i].tokenType.name)) continue
202+
203+
const tableNameResult = readQualifiedName(i + 1)
204+
if (!tableNameResult) continue
205+
206+
let alias: string | undefined
207+
let aliasStart = tableNameResult.nextIndex
208+
if (tokens[aliasStart]?.tokenType.name === "As") {
209+
aliasStart++
210+
}
211+
if (isIdentifierLike(tokens[aliasStart])) {
212+
alias = tokenToNamePart(tokens[aliasStart])
213+
}
214+
215+
tables.push({
216+
table: tableNameResult.name,
217+
alias,
218+
})
219+
220+
// Continue from where we consumed table/alias to avoid duplicate captures.
221+
i = alias ? aliasStart : tableNameResult.nextIndex - 1
192222
}
193223

194224
return tables
@@ -227,18 +257,27 @@ function isImplicitStatementPath(
227257
*
228258
* Example: [SELECT, t, ., col] → [SELECT, col]
229259
*/
260+
/**
261+
* Check if a token type name represents an identifier-like token.
262+
* This includes plain Identifier, QuotedIdentifier, and any keyword
263+
* that has the IdentifierKeyword category (e.g., Timestamp, Index, Type).
264+
*/
265+
function isIdentifierLikeTokenName(name: string): boolean {
266+
return (
267+
name === "Identifier" ||
268+
name === "QuotedIdentifier" ||
269+
name === "IdentifierKeyword" ||
270+
IDENTIFIER_KEYWORD_TOKENS.has(name)
271+
)
272+
}
273+
230274
function collapseTrailingQualifiedRef(tokens: IToken[]): IToken[] | null {
231275
if (tokens.length < 3) return null
232276

233277
// Walk backwards from end: expect Identifier (. Identifier)+ pattern
234278
const i = tokens.length - 1
235279
const lastToken = tokens[i]
236-
const lastType = lastToken.tokenType.name
237-
if (
238-
lastType !== "Identifier" &&
239-
lastType !== "QuotedIdentifier" &&
240-
lastType !== "IdentifierKeyword"
241-
) {
280+
if (!isIdentifierLikeTokenName(lastToken.tokenType.name)) {
242281
return null
243282
}
244283

@@ -248,13 +287,7 @@ function collapseTrailingQualifiedRef(tokens: IToken[]): IToken[] | null {
248287
const maybeDot = tokens[start - 1]
249288
const maybeIdent = tokens[start - 2]
250289
if (maybeDot.tokenType.name !== "Dot") break
251-
const identType = maybeIdent.tokenType.name
252-
if (
253-
identType !== "Identifier" &&
254-
identType !== "QuotedIdentifier" &&
255-
identType !== "IdentifierKeyword"
256-
)
257-
break
290+
if (!isIdentifierLikeTokenName(maybeIdent.tokenType.name)) break
258291
start -= 2
259292
}
260293

@@ -350,6 +383,32 @@ function computeSuggestions(tokens: IToken[]): TokenType[] {
350383
return result
351384
}
352385

386+
/**
387+
* When tablesInScope is empty (no FROM/JOIN yet), try to infer a table name
388+
* from a trailing qualified reference before the cursor.
389+
* e.g. "SELECT trades." → infer "trades" as table in scope.
390+
*/
391+
function inferTableFromQualifiedRef(
392+
tokensBefore: IToken[],
393+
isMidWord: boolean,
394+
): TableRef | null {
395+
// If mid-word, the last token is a partial column name — look before it.
396+
const effective = isMidWord ? tokensBefore.slice(0, -1) : tokensBefore
397+
const lastIdx = effective.length - 1
398+
if (lastIdx < 1) return null
399+
if (effective[lastIdx].tokenType.name !== "Dot") return null
400+
401+
const tableToken = effective[lastIdx - 1]
402+
if (!isIdentifierLikeTokenName(tableToken.tokenType.name)) return null
403+
404+
const table =
405+
tableToken.tokenType.name === "QuotedIdentifier"
406+
? tableToken.image.slice(1, -1)
407+
: tableToken.image
408+
409+
return { table }
410+
}
411+
353412
/**
354413
* Get content assist suggestions for a SQL string at a given cursor position
355414
*
@@ -424,6 +483,11 @@ export function getContentAssist(
424483
const fullTokens = QuestDBLexer.tokenize(fullSql).tokens
425484
const tablesInScope = extractTables(fullSql, fullTokens)
426485

486+
if (tablesInScope.length === 0) {
487+
const inferred = inferTableFromQualifiedRef(tokens, isMidWord)
488+
if (inferred) tablesInScope.push(inferred)
489+
}
490+
427491
return {
428492
nextTokenTypes,
429493
tablesInScope,

0 commit comments

Comments
 (0)