@@ -76,6 +76,10 @@ export interface ContentAssistResult {
7676 * should resolve this against tablesInScope aliases/names to filter columns.
7777 */
7878 qualifiedTableRef ?: string
79+ /** Whether the grammar context expects column names (expression/columnRef positions) */
80+ suggestColumns : boolean
81+ /** Whether the grammar context expects table names (tableName positions, or expression context) */
82+ suggestTables : boolean
7983}
8084
8185// =============================================================================
@@ -239,6 +243,23 @@ function extractTablesFromAst(ast: unknown): ExtractResult {
239243 }
240244 }
241245
246+ // Handle ALTER TABLE / ALTER MATERIALIZED VIEW
247+ if (
248+ ( n . type === "alterTable" || n . type === "alterMaterializedView" ) &&
249+ n . table
250+ ) {
251+ const tableName = normalizeTableName ( n . table )
252+ if ( tableName ) {
253+ tables . push ( { table : tableName } )
254+ }
255+ }
256+ if ( n . type === "alterMaterializedView" && n . view ) {
257+ const viewName = normalizeTableName ( n . view )
258+ if ( viewName ) {
259+ tables . push ( { table : viewName } )
260+ }
261+ }
262+
242263 // Recurse into child nodes
243264 for ( const key of Object . keys ( n ) ) {
244265 const child = n [ key ]
@@ -511,28 +532,46 @@ function extractTables(fullSql: string, tokens: IToken[]): ExtractResult {
511532
512533 // Scan for FROM/JOIN table references only in the outer query (after CTEs).
513534 // This avoids leaking tables referenced inside CTE bodies into the outer scope.
535+ // Also detect ALTER TABLE / TRUNCATE TABLE patterns for column scoping.
536+ const DDL_TABLE_PREFIXES = new Set ( [ "Alter" , "Truncate" , "Drop" ] )
514537 for ( let i = outerQueryStart ; i < tokens . length ; i ++ ) {
515- if ( ! TABLE_PREFIX_TOKENS . has ( tokens [ i ] . tokenType . name ) ) continue
538+ const tokenName = tokens [ i ] . tokenType . name
516539
517- const tableNameResult = readQualifiedName ( i + 1 )
518- if ( ! tableNameResult ) continue
540+ // Standard DML: FROM/JOIN/UPDATE/INTO <table>
541+ if ( TABLE_PREFIX_TOKENS . has ( tokenName ) ) {
542+ const tableNameResult = readQualifiedName ( i + 1 )
543+ if ( ! tableNameResult ) continue
519544
520- let alias : string | undefined
521- let aliasStart = tableNameResult . nextIndex
522- if ( tokens [ aliasStart ] ?. tokenType . name === "As" ) {
523- aliasStart ++
524- }
525- if ( isIdentifierLike ( tokens [ aliasStart ] ) ) {
526- alias = tokenToNamePart ( tokens [ aliasStart ] )
527- }
545+ let alias : string | undefined
546+ let aliasStart = tableNameResult . nextIndex
547+ if ( tokens [ aliasStart ] ?. tokenType . name === "As" ) {
548+ aliasStart ++
549+ }
550+ if ( isIdentifierLike ( tokens [ aliasStart ] ) ) {
551+ alias = tokenToNamePart ( tokens [ aliasStart ] )
552+ }
528553
529- tables . push ( {
530- table : tableNameResult . name ,
531- alias,
532- } )
554+ tables . push ( {
555+ table : tableNameResult . name ,
556+ alias,
557+ } )
558+
559+ // Continue from where we consumed table/alias to avoid duplicate captures.
560+ i = alias ? aliasStart : tableNameResult . nextIndex - 1
561+ continue
562+ }
533563
534- // Continue from where we consumed table/alias to avoid duplicate captures.
535- i = alias ? aliasStart : tableNameResult . nextIndex - 1
564+ // DDL: ALTER TABLE / TRUNCATE TABLE / DROP TABLE <name>
565+ if (
566+ DDL_TABLE_PREFIXES . has ( tokenName ) &&
567+ tokens [ i + 1 ] ?. tokenType . name === "Table"
568+ ) {
569+ const tableNameResult = readQualifiedName ( i + 2 )
570+ if ( tableNameResult ) {
571+ tables . push ( { table : tableNameResult . name } )
572+ i = tableNameResult . nextIndex - 1
573+ }
574+ }
536575 }
537576 for ( const name of cteNames ) {
538577 tables . push ( { table : name } )
@@ -615,6 +654,32 @@ function collapseTrailingQualifiedRef(tokens: IToken[]): IToken[] | null {
615654 return [ ...tokens . slice ( 0 , start ) , lastToken ]
616655}
617656
657+ /**
658+ * Classify an identifier suggestion path based on its ruleStack.
659+ * - "column": identifierExpression or columnRef → suggest columns + tables
660+ * - "table": tableName rule → suggest tables only
661+ * - "newName": everything else (CREATE TABLE name, user names, etc.) → no suggestions
662+ */
663+ function classifyIdentifierPath (
664+ ruleStack : string [ ] ,
665+ ) : "column" | "table" | "newName" {
666+ if ( ruleStack . includes ( "valuesClause" ) ) return "newName"
667+ if (
668+ ruleStack . includes ( "identifierExpression" ) ||
669+ ruleStack . includes ( "columnRef" ) ||
670+ ruleStack . includes ( "qualifiedStar" )
671+ )
672+ return "column"
673+ if ( ruleStack . includes ( "tableName" ) ) return "table"
674+ return "newName"
675+ }
676+
677+ interface ComputeResult {
678+ nextTokenTypes : TokenType [ ]
679+ suggestColumns : boolean
680+ suggestTables : boolean
681+ }
682+
618683/**
619684 * Compute content assist suggestions, handling CTE context specially.
620685 *
@@ -623,7 +688,7 @@ function collapseTrailingQualifiedRef(tokens: IToken[]): IToken[] | null {
623688 * updateStatement paths. This function detects that case and merges suggestions
624689 * from all WITH-capable statement types.
625690 */
626- function computeSuggestions ( tokens : IToken [ ] ) : TokenType [ ] {
691+ function computeSuggestions ( tokens : IToken [ ] ) : ComputeResult {
627692 const ruleName = tokens . some ( ( t ) => t . tokenType . name === "Semicolon" )
628693 ? "statements"
629694 : "statement"
@@ -638,37 +703,58 @@ function computeSuggestions(tokens: IToken[]): TokenType[] {
638703 const specific = suggestions . filter (
639704 ( s ) => ! isImplicitStatementPath ( s . ruleStack , IMPLICIT_RULES ) ,
640705 )
641- const result = ( specific . length > 0 ? specific : suggestions ) . map (
642- ( s ) => s . nextTokenType ,
643- )
706+ const effectiveSuggestions = specific . length > 0 ? specific : suggestions
707+ const result = effectiveSuggestions . map ( ( s ) => s . nextTokenType )
708+
709+ // Classify each IdentifierKeyword path to determine whether columns/tables
710+ // should be suggested, based on the grammar rule that expects the identifier.
711+ let suggestColumns = false
712+ let suggestTables = false
713+ for ( const s of effectiveSuggestions ) {
714+ if ( s . nextTokenType . name === "IdentifierKeyword" ) {
715+ const cls = classifyIdentifierPath ( s . ruleStack )
716+ if ( cls === "column" ) {
717+ suggestColumns = true
718+ suggestTables = true
719+ } else if ( cls === "table" ) {
720+ suggestTables = true
721+ }
722+ }
723+ }
644724
645725 // qualifiedStar fix: When computeContentAssist finds the qualifiedStar
646726 // path in selectItem (suggesting just Dot), the expression path is missed.
647727 // Detect this by checking if the *specific* (non-catch-all) suggestions are
648728 // all from qualifiedStar, then re-compute with the qualified reference
649729 // collapsed to a single identifier to get expression-path suggestions.
650- const effectiveSuggestions = specific . length > 0 ? specific : suggestions
651730 if (
652731 effectiveSuggestions . length > 0 &&
653732 effectiveSuggestions . every ( ( s ) => s . ruleStack . includes ( "qualifiedStar" ) )
654733 ) {
655- // Find and collapse the trailing qualified reference (ident.ident...ident)
656- // into a single identifier token, then re-compute to get expression-path suggestions.
657734 const collapsed = collapseTrailingQualifiedRef ( tokens )
658735 if ( collapsed ) {
659736 try {
660737 const extra = parser . computeContentAssist ( ruleName , collapsed )
661738 const filteredExtra = extra . filter (
662739 ( s ) => ! isImplicitStatementPath ( s . ruleStack , IMPLICIT_RULES ) ,
663740 )
664- const extraResult = (
741+ const extraEffective =
665742 filteredExtra . length > 0 ? filteredExtra : extra
666- ) . map ( ( s ) => s . nextTokenType )
667743 const seen = new Set ( result . map ( ( t ) => t . name ) )
668- for ( const t of extraResult ) {
669- if ( ! seen . has ( t . name ) ) {
670- seen . add ( t . name )
671- result . push ( t )
744+ for ( const s of extraEffective ) {
745+ if ( ! seen . has ( s . nextTokenType . name ) ) {
746+ seen . add ( s . nextTokenType . name )
747+ result . push ( s . nextTokenType )
748+ }
749+ // Classify extra paths too
750+ if ( s . nextTokenType . name === "IdentifierKeyword" ) {
751+ const cls = classifyIdentifierPath ( s . ruleStack )
752+ if ( cls === "column" ) {
753+ suggestColumns = true
754+ suggestTables = true
755+ } else if ( cls === "table" ) {
756+ suggestTables = true
757+ }
672758 }
673759 }
674760 } catch ( e ) {
@@ -677,7 +763,7 @@ function computeSuggestions(tokens: IToken[]): TokenType[] {
677763 }
678764 }
679765
680- return result
766+ return { nextTokenTypes : result , suggestColumns , suggestTables }
681767}
682768
683769/**
@@ -738,6 +824,8 @@ export function getContentAssist(
738824 tokensBefore : [ ] ,
739825 isMidWord : true ,
740826 lexErrors : [ ] ,
827+ suggestColumns : false ,
828+ suggestTables : false ,
741829 }
742830 }
743831 }
@@ -764,8 +852,13 @@ export function getContentAssist(
764852
765853 // Get syntactically valid next tokens using Chevrotain's content assist
766854 let nextTokenTypes : TokenType [ ] = [ ]
855+ let suggestColumns = false
856+ let suggestTables = false
767857 try {
768- nextTokenTypes = computeSuggestions ( tokensForAssist )
858+ const computed = computeSuggestions ( tokensForAssist )
859+ nextTokenTypes = computed . nextTokenTypes
860+ suggestColumns = computed . suggestColumns
861+ suggestTables = computed . suggestTables
769862 } catch ( e ) {
770863 // If content assist fails, return empty suggestions
771864 // This can happen with malformed input
@@ -834,6 +927,8 @@ export function getContentAssist(
834927 isMidWord,
835928 lexErrors : lexResult . errors ,
836929 qualifiedTableRef : qualifiedRef ?. table ,
930+ suggestColumns,
931+ suggestTables,
837932 }
838933}
839934
@@ -843,7 +938,9 @@ export function getContentAssist(
843938export function getNextValidTokens ( sql : string ) : string [ ] {
844939 const lexResult = QuestDBLexer . tokenize ( sql )
845940 try {
846- return computeSuggestions ( lexResult . tokens ) . map ( ( t ) => t . name )
941+ return computeSuggestions ( lexResult . tokens ) . nextTokenTypes . map (
942+ ( t ) => t . name ,
943+ )
847944 } catch ( e ) {
848945 return [ ]
849946 }
0 commit comments