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
43 changes: 32 additions & 11 deletions packages/server/src/complete/candidates/createTableCandidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ export function createCatalogDatabaseAndTableCandidates(
const qualificationLevel = lastToken.split('.').length - 1

const qualifiedEntities = tables.flatMap((table) => {
const results: Identifier[] = []

// When user types without dots (qualificationLevel === 0), always include
// a table name suggestion. This allows typing "act" to match "actor" even
// if the table has a database (e.g., "squeal.actor").
if (qualificationLevel === 0) {
const tableIdentifier = new Identifier(
lastToken,
table.tableName,
'',
ICONS.TABLE,
onFromClause ? 'FROM' : 'OTHERS'
)
results.push(tableIdentifier)
}

// Also add qualified suggestions (catalog/database) based on qualification level
let qualificationNeeded = 0
if (table.catalog) {
qualificationNeeded++
Expand All @@ -37,14 +54,18 @@ export function createCatalogDatabaseAndTableCandidates(
const qualificationLevelNeeded = qualificationNeeded - qualificationLevel
switch (qualificationLevelNeeded) {
case 0: {
const tableIdentifier = new Identifier(
lastToken,
getFullyQualifiedTableName(table),
'',
ICONS.TABLE,
onFromClause ? 'FROM' : 'OTHERS'
)
return [tableIdentifier]
// Only add fully qualified name if we haven't already added just the table name
if (qualificationLevel > 0) {
const tableIdentifier = new Identifier(
lastToken,
getFullyQualifiedTableName(table),
'',
ICONS.TABLE,
onFromClause ? 'FROM' : 'OTHERS'
)
results.push(tableIdentifier)
}
break
}
case 1: {
const qualifiedDatabaseName =
Expand All @@ -60,7 +81,7 @@ export function createCatalogDatabaseAndTableCandidates(
ICONS.DATABASE,
onFromClause ? 'FROM' : 'OTHERS'
)
return [databaseIdentifier]
results.push(databaseIdentifier)
}
break
}
Expand All @@ -73,11 +94,11 @@ export function createCatalogDatabaseAndTableCandidates(
ICONS.CATALOG,
onFromClause ? 'FROM' : 'OTHERS'
)
return [catalogIdentifier]
results.push(catalogIdentifier)
}
break
}
return []
return results
})

return qualifiedEntities
Expand Down
35 changes: 31 additions & 4 deletions packages/server/src/complete/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,14 +350,41 @@ class Completer {
if (!ast.distinct) {
this.addCandidate(toCompletionItemForKeyword('DISTINCT'))
}

// Check if cursor is inside a FROM clause table reference
// This handles the case where "SELECT * FROM a" parses successfully
// but we still want to suggest tables starting with "a"
const parsedFromClause = getFromNodesFromClause(this.sql)
const fromNodes = getAllNestedFromNodes(
parsedFromClause?.from?.tables || []
)
const subqueryTables = createTablesFromFromNodes(fromNodes)
const schemaAndSubqueries = this.schema.tables.concat(subqueryTables)

for (const tableNode of fromNodes) {
if (tableNode.type === 'table') {
// Check if the lastToken matches the table name (user is typing the table name)
// This means the cursor is ON the table name, not after it (like typing an alias)
const tableNameMatches =
this.lastToken.length > 0 &&
tableNode.table.toLowerCase().startsWith(this.lastToken.toLowerCase())

if (tableNameMatches && isPosInLocation(tableNode.location, this.pos)) {
// Cursor is typing a table name - suggest tables
this.addCandidatesForTables(schemaAndSubqueries, true)
if (logger.isDebugEnabled())
logger.debug(
`parse query returns: ${JSON.stringify(this.candidates)}`
)
return
}
}
}

const columnRef = findColumnAtPosition(ast, this.pos)
if (!columnRef) {
this.addJoinCondidates(ast)
} else {
const parsedFromClause = getFromNodesFromClause(this.sql)
const fromNodes = parsedFromClause?.from?.tables || []
const subqueryTables = createTablesFromFromNodes(fromNodes)
const schemaAndSubqueries = this.schema.tables.concat(subqueryTables)
if (columnRef.table) {
// We know what table/alias this column belongs to
// Find the corresponding table and suggest it's columns
Expand Down
10 changes: 8 additions & 2 deletions packages/server/test/complete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,19 @@ describe('Fully qualified table names', () => {
expect(result.candidates).toEqual(expect.arrayContaining(expected))
})

test('not complete table name when not qualified', () => {
test('complete table name when not qualified', () => {
// After the fix for GitHub issue #24, typing a partial table name should
// match tables even if they require qualification (have database/catalog).
// This allows users to type "tabl" and get "table2" and "table3" suggestions.
const result = complete(
'SELECT * FROM tabl',
{ line: 0, column: 18 },
SIMPLE_NESTED_SCHEMA
)
expect(result.candidates.length).toEqual(0)
// Should match table2 and table3
const labels = result.candidates.map((c) => c.label)
expect(labels).toContain('table2')
expect(labels).toContain('table3')
})
test('complete alias when table', () => {
const result = complete(
Expand Down
Loading