diff --git a/packages/server/src/complete/complete.ts b/packages/server/src/complete/complete.ts index 5ca934cf..f73d4e52 100644 --- a/packages/server/src/complete/complete.ts +++ b/packages/server/src/complete/complete.ts @@ -111,7 +111,7 @@ class Completer { // Incomplete sub query 'SELECT sub FROM (SELECT e. FROM employees e) sub' this.addCandidatesForIncompleteSubquery(fromNodeOnCursor) } else { - this.addCandidatesForSelectQuery(e, fromNodes) + this.addCandidatesForSelectQuery(e, fromNodes, parsedFromClause) const expectedLiteralNodes = e.expected?.filter( (v): v is ExpectedLiteralNode => v.type === 'literal' @@ -239,7 +239,11 @@ class Completer { this.addCandidatesForTables(this.schema.tables, false) } - addCandidatesForSelectQuery(e: ParseError, fromNodes: FromTableNode[]) { + addCandidatesForSelectQuery( + e: ParseError, + fromNodes: FromTableNode[], + parsedFromClause: FromClauseParserResult + ) { const subqueryTables = createTablesFromFromNodes(fromNodes) const schemaAndSubqueries = this.schema.tables.concat(subqueryTables) this.addCandidatesForSelectStar(fromNodes, schemaAndSubqueries) @@ -263,8 +267,19 @@ class Completer { isPosInLocation(tableNode.location, this.pos) ) const isCursorInsideFromClause = fromNodesContainingCursor.length > 0 - if (isCursorInsideFromClause) { - // only add table candidates if the cursor is inside a FROM clause or JOIN clause, etc. + + // Check if cursor is right after FROM keyword (no table typed yet) + const afterFromClause = parsedFromClause.after?.trim().toUpperCase() || '' + const isCursorAfterFromKeyword = + afterFromClause === 'FROM' || + afterFromClause.startsWith('FROM ') || + /^(INNER |LEFT |RIGHT |FULL |FULL OUTER |CROSS |NATURAL |OUTER )?JOIN( |$)/.test( + afterFromClause + ) + + if (isCursorInsideFromClause || isCursorAfterFromKeyword) { + // add table candidates if the cursor is inside a FROM clause, JOIN clause, + // or right after FROM/JOIN keyword waiting for a table name this.addCandidatesForTables(schemaAndSubqueries, true) } diff --git a/packages/server/test/complete.test.ts b/packages/server/test/complete.test.ts index f02e747b..b00d1434 100644 --- a/packages/server/test/complete.test.ts +++ b/packages/server/test/complete.test.ts @@ -241,6 +241,24 @@ describe('From clause', () => { expect(result.candidates[0].insertText).toEqual('TABLE1') }) + test('from clause: complete TableName after FROM keyword with space', () => { + const result = complete( + 'SELECT * FROM ', + { line: 0, column: 14 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after FROM keyword with space:multi line', () => { + const result = complete( + 'SELECT *\nFROM ', + { line: 1, column: 5 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + test('from clause: complete TableName:multi lines', () => { const result = complete( 'SELECT TABLE1.COLUMN1\nFROM T', @@ -277,6 +295,69 @@ describe('From clause', () => { ) expect(result.candidates.map((v) => v.label)).toContain('ON') }) + + test('from clause: complete TableName after JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 JOIN ', + { line: 0, column: 26 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after INNER JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 INNER JOIN ', + { line: 0, column: 32 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after LEFT JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 LEFT JOIN ', + { line: 0, column: 31 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after RIGHT JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 RIGHT JOIN ', + { line: 0, column: 32 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after CROSS JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 CROSS JOIN ', + { line: 0, column: 32 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after FULL OUTER JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 FULL OUTER JOIN ', + { line: 0, column: 37 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) + + test('from clause: complete TableName after NATURAL JOIN keyword with space', () => { + const result = complete( + 'SELECT * FROM TABLE1 NATURAL JOIN ', + { line: 0, column: 34 }, + SIMPLE_SCHEMA + ) + expect(result.candidates.map((v) => v.label)).toContain('TABLE1') + }) }) describe('Where clause', () => {