Skip to content

Commit 151e1f3

Browse files
authored
fix: Fix autocomplete table name issues. (#25)
1 parent 42e9e81 commit 151e1f3

File tree

4 files changed

+228
-55
lines changed

4 files changed

+228
-55
lines changed

packages/server/src/complete/candidates/createTableCandidates.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ export function createCatalogDatabaseAndTableCandidates(
2727
const qualificationLevel = lastToken.split('.').length - 1
2828

2929
const qualifiedEntities = tables.flatMap((table) => {
30+
const results: Identifier[] = []
31+
32+
// When user types without dots (qualificationLevel === 0), always include
33+
// a table name suggestion. This allows typing "act" to match "actor" even
34+
// if the table has a database (e.g., "squeal.actor").
35+
if (qualificationLevel === 0) {
36+
const tableIdentifier = new Identifier(
37+
lastToken,
38+
table.tableName,
39+
'',
40+
ICONS.TABLE,
41+
onFromClause ? 'FROM' : 'OTHERS'
42+
)
43+
results.push(tableIdentifier)
44+
}
45+
46+
// Also add qualified suggestions (catalog/database) based on qualification level
3047
let qualificationNeeded = 0
3148
if (table.catalog) {
3249
qualificationNeeded++
@@ -37,14 +54,18 @@ export function createCatalogDatabaseAndTableCandidates(
3754
const qualificationLevelNeeded = qualificationNeeded - qualificationLevel
3855
switch (qualificationLevelNeeded) {
3956
case 0: {
40-
const tableIdentifier = new Identifier(
41-
lastToken,
42-
getFullyQualifiedTableName(table),
43-
'',
44-
ICONS.TABLE,
45-
onFromClause ? 'FROM' : 'OTHERS'
46-
)
47-
return [tableIdentifier]
57+
// Only add fully qualified name if we haven't already added just the table name
58+
if (qualificationLevel > 0) {
59+
const tableIdentifier = new Identifier(
60+
lastToken,
61+
getFullyQualifiedTableName(table),
62+
'',
63+
ICONS.TABLE,
64+
onFromClause ? 'FROM' : 'OTHERS'
65+
)
66+
results.push(tableIdentifier)
67+
}
68+
break
4869
}
4970
case 1: {
5071
const qualifiedDatabaseName =
@@ -60,7 +81,7 @@ export function createCatalogDatabaseAndTableCandidates(
6081
ICONS.DATABASE,
6182
onFromClause ? 'FROM' : 'OTHERS'
6283
)
63-
return [databaseIdentifier]
84+
results.push(databaseIdentifier)
6485
}
6586
break
6687
}
@@ -73,11 +94,11 @@ export function createCatalogDatabaseAndTableCandidates(
7394
ICONS.CATALOG,
7495
onFromClause ? 'FROM' : 'OTHERS'
7596
)
76-
return [catalogIdentifier]
97+
results.push(catalogIdentifier)
7798
}
7899
break
79100
}
80-
return []
101+
return results
81102
})
82103

83104
return qualifiedEntities

packages/server/src/complete/complete.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,41 @@ class Completer {
350350
if (!ast.distinct) {
351351
this.addCandidate(toCompletionItemForKeyword('DISTINCT'))
352352
}
353+
354+
// Check if cursor is inside a FROM clause table reference
355+
// This handles the case where "SELECT * FROM a" parses successfully
356+
// but we still want to suggest tables starting with "a"
357+
const parsedFromClause = getFromNodesFromClause(this.sql)
358+
const fromNodes = getAllNestedFromNodes(
359+
parsedFromClause?.from?.tables || []
360+
)
361+
const subqueryTables = createTablesFromFromNodes(fromNodes)
362+
const schemaAndSubqueries = this.schema.tables.concat(subqueryTables)
363+
364+
for (const tableNode of fromNodes) {
365+
if (tableNode.type === 'table') {
366+
// Check if the lastToken matches the table name (user is typing the table name)
367+
// This means the cursor is ON the table name, not after it (like typing an alias)
368+
const tableNameMatches =
369+
this.lastToken.length > 0 &&
370+
tableNode.table.toLowerCase().startsWith(this.lastToken.toLowerCase())
371+
372+
if (tableNameMatches && isPosInLocation(tableNode.location, this.pos)) {
373+
// Cursor is typing a table name - suggest tables
374+
this.addCandidatesForTables(schemaAndSubqueries, true)
375+
if (logger.isDebugEnabled())
376+
logger.debug(
377+
`parse query returns: ${JSON.stringify(this.candidates)}`
378+
)
379+
return
380+
}
381+
}
382+
}
383+
353384
const columnRef = findColumnAtPosition(ast, this.pos)
354385
if (!columnRef) {
355386
this.addJoinCondidates(ast)
356387
} else {
357-
const parsedFromClause = getFromNodesFromClause(this.sql)
358-
const fromNodes = parsedFromClause?.from?.tables || []
359-
const subqueryTables = createTablesFromFromNodes(fromNodes)
360-
const schemaAndSubqueries = this.schema.tables.concat(subqueryTables)
361388
if (columnRef.table) {
362389
// We know what table/alias this column belongs to
363390
// Find the corresponding table and suggest it's columns

packages/server/test/complete.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,13 +553,19 @@ describe('Fully qualified table names', () => {
553553
expect(result.candidates).toEqual(expect.arrayContaining(expected))
554554
})
555555

556-
test('not complete table name when not qualified', () => {
556+
test('complete table name when not qualified', () => {
557+
// After the fix for GitHub issue #24, typing a partial table name should
558+
// match tables even if they require qualification (have database/catalog).
559+
// This allows users to type "tabl" and get "table2" and "table3" suggestions.
557560
const result = complete(
558561
'SELECT * FROM tabl',
559562
{ line: 0, column: 18 },
560563
SIMPLE_NESTED_SCHEMA
561564
)
562-
expect(result.candidates.length).toEqual(0)
565+
// Should match table2 and table3
566+
const labels = result.candidates.map((c) => c.label)
567+
expect(labels).toContain('table2')
568+
expect(labels).toContain('table3')
563569
})
564570
test('complete alias when table', () => {
565571
const result = complete(

0 commit comments

Comments
 (0)