@@ -111,7 +111,7 @@ class Completer {
111111 // Incomplete sub query 'SELECT sub FROM (SELECT e. FROM employees e) sub'
112112 this . addCandidatesForIncompleteSubquery ( fromNodeOnCursor )
113113 } else {
114- this . addCandidatesForSelectQuery ( e , fromNodes )
114+ this . addCandidatesForSelectQuery ( e , fromNodes , parsedFromClause )
115115 const expectedLiteralNodes =
116116 e . expected ?. filter (
117117 ( v ) : v is ExpectedLiteralNode => v . type === 'literal'
@@ -239,7 +239,11 @@ class Completer {
239239 this . addCandidatesForTables ( this . schema . tables , false )
240240 }
241241
242- addCandidatesForSelectQuery ( e : ParseError , fromNodes : FromTableNode [ ] ) {
242+ addCandidatesForSelectQuery (
243+ e : ParseError ,
244+ fromNodes : FromTableNode [ ] ,
245+ parsedFromClause : FromClauseParserResult
246+ ) {
243247 const subqueryTables = createTablesFromFromNodes ( fromNodes )
244248 const schemaAndSubqueries = this . schema . tables . concat ( subqueryTables )
245249 this . addCandidatesForSelectStar ( fromNodes , schemaAndSubqueries )
@@ -251,20 +255,38 @@ class Completer {
251255 this . addCandidatesForExpectedLiterals ( expectedLiteralNodes )
252256 this . addCandidatesForFunctions ( )
253257
254- const { addedSome : addedSomeScopedColumnCandidates } =
255- this . addCandidatesForScopedColumns ( fromNodes , schemaAndSubqueries )
256- if ( ! addedSomeScopedColumnCandidates ) {
257- this . addCandidatesForUnscopedColumns ( fromNodes , schemaAndSubqueries )
258- }
259-
260- this . addCandidatesForAliases ( fromNodes )
261-
258+ // Detect FROM clause context BEFORE adding column suggestions
262259 const fromNodesContainingCursor = fromNodes . filter ( ( tableNode ) =>
263260 isPosInLocation ( tableNode . location , this . pos )
264261 )
265262 const isCursorInsideFromClause = fromNodesContainingCursor . length > 0
266- if ( isCursorInsideFromClause ) {
267- // only add table candidates if the cursor is inside a FROM clause or JOIN clause, etc.
263+
264+ // Check if cursor is right after FROM keyword or typing a table name
265+ const afterFromClause = parsedFromClause . after ?. trim ( ) . toUpperCase ( ) || ''
266+ const isCursorAfterFromKeyword =
267+ afterFromClause === 'FROM' ||
268+ afterFromClause . startsWith ( 'FROM ' ) ||
269+ / ^ ( I N N E R | L E F T | R I G H T | F U L L | F U L L O U T E R | C R O S S | N A T U R A L | O U T E R ) ? J O I N ( | $ ) / . test (
270+ afterFromClause
271+ )
272+
273+ const isTypingTableName =
274+ isCursorInsideFromClause || isCursorAfterFromKeyword
275+
276+ if ( ! isTypingTableName ) {
277+ const { addedSome : addedSomeScopedColumnCandidates } =
278+ this . addCandidatesForScopedColumns ( fromNodes , schemaAndSubqueries )
279+
280+ if ( ! addedSomeScopedColumnCandidates ) {
281+ this . addCandidatesForUnscopedColumns ( fromNodes , schemaAndSubqueries )
282+ }
283+ }
284+
285+ this . addCandidatesForAliases ( fromNodes )
286+
287+ if ( isTypingTableName ) {
288+ // add table candidates if the cursor is inside a FROM clause, JOIN clause,
289+ // or right after FROM/JOIN keyword waiting for a table name
268290 this . addCandidatesForTables ( schemaAndSubqueries , true )
269291 }
270292
@@ -328,14 +350,41 @@ class Completer {
328350 if ( ! ast . distinct ) {
329351 this . addCandidate ( toCompletionItemForKeyword ( 'DISTINCT' ) )
330352 }
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+
331384 const columnRef = findColumnAtPosition ( ast , this . pos )
332385 if ( ! columnRef ) {
333386 this . addJoinCondidates ( ast )
334387 } else {
335- const parsedFromClause = getFromNodesFromClause ( this . sql )
336- const fromNodes = parsedFromClause ?. from ?. tables || [ ]
337- const subqueryTables = createTablesFromFromNodes ( fromNodes )
338- const schemaAndSubqueries = this . schema . tables . concat ( subqueryTables )
339388 if ( columnRef . table ) {
340389 // We know what table/alias this column belongs to
341390 // Find the corresponding table and suggest it's columns
0 commit comments