@@ -83,6 +83,14 @@ export interface ContentAssistResult {
8383 suggestColumns : boolean
8484 /** Whether the grammar context expects table names (tableName positions, or expression context) */
8585 suggestTables : boolean
86+ /** Whether scalar functions are valid at this position (any expression context) */
87+ suggestScalarFunctions : boolean
88+ /** Whether aggregate functions are valid at this position (SELECT/ORDER BY/HAVING) */
89+ suggestAggregateFunctions : boolean
90+ /** Whether window functions are valid at this position (SELECT/ORDER BY) */
91+ suggestWindowFunctions : boolean
92+ /** Whether table-valued functions are valid at this position (FROM/JOIN) */
93+ suggestTableValuedFunctions : boolean
8694 /**
8795 * Bare column names (lowercase) referenced before the cursor in expression
8896 * context. Used by the provider to boost tables containing all these columns.
@@ -594,27 +602,6 @@ function extractTables(fullSql: string, tokens: IToken[]): ExtractResult {
594602// Main API
595603// =============================================================================
596604
597- /**
598- * Check if a ruleStack path represents the catch-all implicit SELECT or pivot
599- * at the top-level statement dispatch. These are BACKTRACK alternatives that
600- * computeContentAssist explores unconditionally, producing noise when a
601- * specific statement rule already matched.
602- *
603- * Detects: statement → implicitSelectBody / implicitSelectStatement / pivotStatement (top-level catch-all)
604- * Does NOT match nested uses (e.g., cteDefinition → implicitSelectBody).
605- */
606- function isImplicitStatementPath (
607- ruleStack : string [ ] ,
608- implicitRules : Set < string > ,
609- ) : boolean {
610- for ( let i = 0 ; i < ruleStack . length - 1 ; i ++ ) {
611- if ( ruleStack [ i ] === "statement" && implicitRules . has ( ruleStack [ i + 1 ] ) ) {
612- return true
613- }
614- }
615- return false
616- }
617-
618605/**
619606 * Collapse a trailing qualified reference (ident.ident...ident) in the token
620607 * stream into a single identifier token. This allows re-computing suggestions
@@ -664,30 +651,182 @@ function collapseTrailingQualifiedRef(tokens: IToken[]): IToken[] | null {
664651 return [ ...tokens . slice ( 0 , start ) , lastToken ]
665652}
666653
654+ /**
655+ * Position categories used to drive context-aware suggestion emission.
656+ *
657+ * - "newName" — name being defined (CREATE TABLE <name>): suppress
658+ * all schema/function suggestions.
659+ * - "expression" — expression position where aggregates and windows
660+ * are syntactically valid: SELECT items, ORDER BY.
661+ * - "restrictedExpression"— any expression where aggregates and windows are
662+ * NOT valid: WHERE / GROUP BY / JOIN ON predicates,
663+ * UPDATE SET RHS, INSERT VALUES rows, DECLARE
664+ * assignment RHS.
665+ * - "columnReference" — bare column-name reference with no surrounding
666+ * expression context: ALTER TABLE … DROP COLUMN
667+ * <name>, ALTER TABLE … RENAME COLUMN <name>.
668+ * Columns suggested; functions never valid here.
669+ * - "tableSource" — FROM/JOIN positions: tables + table-valued fns.
670+ * - "tableName" — DROP TABLE / INSERT INTO / UPDATE / TRUNCATE
671+ * TABLE / RENAME TABLE: tables only, no functions.
672+ * - "numeric" — LIMIT / OFFSET: numeric literal expected; nothing
673+ * useful to suggest from schema or functions.
674+ */
675+ export type PositionKind =
676+ | "newName"
677+ | "expression"
678+ | "restrictedExpression"
679+ | "columnReference"
680+ | "tableSource"
681+ | "tableName"
682+ | "numeric"
683+
684+ // Marker rules that, when encountered before any "fresh expression" marker
685+ // while walking the ruleStack innermost-to-outermost, restrict the position
686+ // to scalar-only (no aggregates, no windows). These are positions where SQL
687+ // disallows aggregation: WHERE/GROUP BY/JOIN ON predicates, UPDATE SET, INSERT
688+ // VALUES rows, DECLARE assignments.
689+ const RESTRICTED_EXPRESSION_RULES = new Set ( [
690+ "whereClause" ,
691+ "groupByClause" ,
692+ "setClause" , // UPDATE x SET col = <expr>
693+ "valuesList" , // INSERT INTO t VALUES (<expr>, ...)
694+ "joinClause" , // ... JOIN ... ON <expr>
695+ "declareAssignment" , // DECLARE @x := <expr>
696+ ] )
697+
698+ // Marker rules that introduce a fresh expression context. When walking the
699+ // ruleStack innermost-to-outermost, hitting one of these BEFORE a restriction
700+ // marker means we're inside a nested expression where aggregates/windows are
701+ // valid again — e.g. `WHERE id IN (SELECT count(*) FROM ...)`. The inner
702+ // selectItem overrides the outer whereClause restriction.
703+ const FRESH_EXPRESSION_RULES = new Set ( [ "selectItem" ] )
704+
667705/**
668706 * Classify an identifier suggestion path based on its ruleStack.
669- * - "column": identifierExpression or columnRef → suggest columns + tables
670- * - "table": tableName rule → suggest tables only
671- * - "newName": everything else (CREATE TABLE name, user names, etc.) → no suggestions
707+ * Walks ruleStack markers to map a Chevrotain parse position onto a semantic
708+ * PositionKind that the suggestion-builder can dispatch on.
672709 */
673- function classifyIdentifierPath (
674- ruleStack : string [ ] ,
675- ) : "column" | "table" | "newName" {
676- if ( ruleStack . includes ( "valuesClause" ) ) return "newName"
677- if (
710+ function classifyIdentifierPath ( ruleStack : string [ ] ) : PositionKind {
711+ if ( ruleStack . includes ( "limitClause" ) ) return "numeric"
712+
713+ // Full expression context: the parser explicitly entered identifierExpression
714+ // (the productive expression rule) OR qualifiedStar (`t.*` — only legal in
715+ // selectItem position). Both allow aggregates/windows where applicable.
716+ const hasExpressionMarker =
678717 ruleStack . includes ( "identifierExpression" ) ||
679- ruleStack . includes ( "columnRef" ) ||
680718 ruleStack . includes ( "qualifiedStar" )
681- )
682- return "column"
683- if ( ruleStack . includes ( "tableName" ) ) return "table"
719+ if ( hasExpressionMarker ) {
720+ // Walk innermost-to-outermost. The first scope marker we hit wins:
721+ // - selectItem → fresh expression context, aggregates allowed (handles
722+ // subqueries inside WHERE/SET/JOIN-ON without inheriting restriction)
723+ // - whereClause/groupByClause/etc. → restricted, scalar only
724+ for ( let i = ruleStack . length - 1 ; i >= 0 ; i -- ) {
725+ const r = ruleStack [ i ]
726+ if ( FRESH_EXPRESSION_RULES . has ( r ) ) return "expression"
727+ if ( RESTRICTED_EXPRESSION_RULES . has ( r ) ) return "restrictedExpression"
728+ }
729+ return "expression"
730+ }
731+
732+ // columnRef without an enclosing expression rule = bare column-name reference.
733+ // E.g. ALTER TABLE x DROP COLUMN <name>, CREATE INDEX ON x(<name>).
734+ // Columns are valid; functions and other tables are not.
735+ if ( ruleStack . includes ( "columnRef" ) ) return "columnReference"
736+
737+ if ( ruleStack . includes ( "tableName" ) ) {
738+ // tableSource = under fromClause / joinClause / tableRef (functions OK).
739+ // Anything else (DROP/INSERT INTO/UPDATE/etc.) is a bare table reference
740+ // where table-valued functions don't apply.
741+ if (
742+ ruleStack . includes ( "fromClause" ) ||
743+ ruleStack . includes ( "joinClause" ) ||
744+ ruleStack . includes ( "tableRef" )
745+ ) {
746+ return "tableSource"
747+ }
748+ return "tableName"
749+ }
750+
684751 return "newName"
685752}
686753
687- interface ComputeResult {
688- nextTokenTypes : TokenType [ ]
754+ /**
755+ * Set of category flags that the suggestion-builder consults to decide which
756+ * function and schema buckets to emit.
757+ */
758+ export interface CategoryFlags {
689759 suggestColumns : boolean
690760 suggestTables : boolean
761+ suggestScalarFunctions : boolean
762+ suggestAggregateFunctions : boolean
763+ suggestWindowFunctions : boolean
764+ suggestTableValuedFunctions : boolean
765+ }
766+
767+ /** All flags off — starting point for OR-accumulation across ruleStacks. */
768+ function emptyCategoryFlags ( ) : CategoryFlags {
769+ return {
770+ suggestColumns : false ,
771+ suggestTables : false ,
772+ suggestScalarFunctions : false ,
773+ suggestAggregateFunctions : false ,
774+ suggestWindowFunctions : false ,
775+ suggestTableValuedFunctions : false ,
776+ }
777+ }
778+
779+ /**
780+ * Map a PositionKind to the set of category flags valid at that position.
781+ * The suggestion-builder unions flags from every ruleStack the parser explored.
782+ */
783+ function flagsForPosition ( kind : PositionKind ) : Partial < CategoryFlags > {
784+ switch ( kind ) {
785+ case "expression" :
786+ return {
787+ suggestColumns : true ,
788+ suggestTables : true ,
789+ suggestScalarFunctions : true ,
790+ suggestAggregateFunctions : true ,
791+ suggestWindowFunctions : true ,
792+ }
793+ case "restrictedExpression" :
794+ return {
795+ suggestColumns : true ,
796+ suggestTables : true ,
797+ suggestScalarFunctions : true ,
798+ }
799+ case "columnReference" :
800+ // Bare column name (e.g. ALTER DROP COLUMN <name>) — only columns of
801+ // the relevant table. No functions, no other tables.
802+ return {
803+ suggestColumns : true ,
804+ }
805+ case "tableSource" :
806+ return {
807+ suggestTables : true ,
808+ suggestTableValuedFunctions : true ,
809+ }
810+ case "tableName" :
811+ return {
812+ suggestTables : true ,
813+ }
814+ case "numeric" :
815+ case "newName" :
816+ return { }
817+ }
818+ }
819+
820+ /** Accumulate (OR) the flags for one PositionKind into the running set. */
821+ function accumulateFlags ( target : CategoryFlags , kind : PositionKind ) : void {
822+ const partial = flagsForPosition ( kind )
823+ for ( const key of Object . keys ( partial ) as ( keyof CategoryFlags ) [ ] ) {
824+ if ( partial [ key ] ) target [ key ] = true
825+ }
826+ }
827+
828+ interface ComputeResult extends CategoryFlags {
829+ nextTokenTypes : TokenType [ ]
691830 isConditionContext : boolean
692831}
693832
@@ -698,73 +837,53 @@ interface ComputeResult {
698837 * the insertStatement path when WITH is present, missing selectStatement and
699838 * updateStatement paths. This function detects that case and merges suggestions
700839 * from all WITH-capable statement types.
840+ *
841+ * Note on the implicit-SELECT / pivot backtrack paths: a previous version of
842+ * this file filtered ruleStacks that traversed `implicitSelectBody` /
843+ * `implicitSelectStatement` / `pivotStatement` to suppress noise when a
844+ * specific statement rule had matched. That filter was removed because the
845+ * category flags below already gate function emission per PositionKind, so
846+ * the implicit/pivot paths can only contribute tableSource-style identifier
847+ * suggestions — which is the desired behaviour for shorthand table queries.
701848 */
702849function computeSuggestions ( tokens : IToken [ ] ) : ComputeResult {
703850 const ruleName = tokens . some ( ( t ) => t . tokenType . name === "Semicolon" )
704851 ? "statements"
705852 : "statement"
706853 const suggestions = parser . computeContentAssist ( ruleName , tokens )
854+ const result = suggestions . map ( ( s ) => s . nextTokenType )
707855
708- // Filter out noise from implicit SELECT / pivot catch-all paths.
709- const IMPLICIT_RULES = new Set ( [
710- "implicitSelectBody" ,
711- "implicitSelectStatement" ,
712- "pivotStatement" ,
713- ] )
714- const specific = suggestions . filter (
715- ( s ) => ! isImplicitStatementPath ( s . ruleStack , IMPLICIT_RULES ) ,
716- )
717- const effectiveSuggestions = specific . length > 0 ? specific : suggestions
718- const result = effectiveSuggestions . map ( ( s ) => s . nextTokenType )
719-
720- // Classify each IdentifierKeyword path to determine whether columns/tables
721- // should be suggested, based on the grammar rule that expects the identifier.
722- let suggestColumns = false
723- let suggestTables = false
724- for ( const s of effectiveSuggestions ) {
856+ // Walk every IdentifierKeyword path and union the category flags valid at
857+ // that position. Multiple paths may be alive simultaneously (Chevrotain
858+ // explores all reachable alternatives) — OR-ing covers all valid contexts.
859+ const flags = emptyCategoryFlags ( )
860+ for ( const s of suggestions ) {
725861 if ( s . nextTokenType . name === "IdentifierKeyword" ) {
726- const cls = classifyIdentifierPath ( s . ruleStack )
727- if ( cls === "column" ) {
728- suggestColumns = true
729- suggestTables = true
730- } else if ( cls === "table" ) {
731- suggestTables = true
732- }
862+ accumulateFlags ( flags , classifyIdentifierPath ( s . ruleStack ) )
733863 }
734864 }
735865
736866 // qualifiedStar fix: When computeContentAssist finds the qualifiedStar
737867 // path in selectItem (suggesting just Dot), the expression path is missed.
738- // Detect this by checking if the *specific* (non-catch- all) suggestions are
739- // all from qualifiedStar, then re-compute with the qualified reference
740- // collapsed to a single identifier to get expression-path suggestions.
868+ // Detect this by checking if the suggestions are all from qualifiedStar,
869+ // then re-compute with the qualified reference collapsed to a single
870+ // identifier to get expression-path suggestions.
741871 if (
742- effectiveSuggestions . length > 0 &&
743- effectiveSuggestions . every ( ( s ) => s . ruleStack . includes ( "qualifiedStar" ) )
872+ suggestions . length > 0 &&
873+ suggestions . every ( ( s ) => s . ruleStack . includes ( "qualifiedStar" ) )
744874 ) {
745875 const collapsed = collapseTrailingQualifiedRef ( tokens )
746876 if ( collapsed ) {
747877 try {
748878 const extra = parser . computeContentAssist ( ruleName , collapsed )
749- const filteredExtra = extra . filter (
750- ( s ) => ! isImplicitStatementPath ( s . ruleStack , IMPLICIT_RULES ) ,
751- )
752- const extraEffective = filteredExtra . length > 0 ? filteredExtra : extra
753879 const seen = new Set ( result . map ( ( t ) => t . name ) )
754- for ( const s of extraEffective ) {
880+ for ( const s of extra ) {
755881 if ( ! seen . has ( s . nextTokenType . name ) ) {
756882 seen . add ( s . nextTokenType . name )
757883 result . push ( s . nextTokenType )
758884 }
759- // Classify extra paths too
760885 if ( s . nextTokenType . name === "IdentifierKeyword" ) {
761- const cls = classifyIdentifierPath ( s . ruleStack )
762- if ( cls === "column" ) {
763- suggestColumns = true
764- suggestTables = true
765- } else if ( cls === "table" ) {
766- suggestTables = true
767- }
886+ accumulateFlags ( flags , classifyIdentifierPath ( s . ruleStack ) )
768887 }
769888 }
770889 } catch ( e ) {
@@ -776,16 +895,15 @@ function computeSuggestions(tokens: IToken[]): ComputeResult {
776895 // Check if an expression operator's ruleStack includes "whereClause".
777896 // Must check operators specifically — Chevrotain explores ahead into
778897 // not-yet-started WHERE paths even from JOIN ON positions.
779- const isConditionContext = effectiveSuggestions . some (
898+ const isConditionContext = suggestions . some (
780899 ( s ) =>
781900 EXPRESSION_OPERATORS . has ( s . nextTokenType . name ) &&
782901 s . ruleStack . includes ( "whereClause" ) ,
783902 )
784903
785904 return {
786905 nextTokenTypes : result ,
787- suggestColumns,
788- suggestTables,
906+ ...flags ,
789907 isConditionContext,
790908 }
791909}
@@ -921,6 +1039,10 @@ export function getContentAssist(
9211039 lexErrors : [ ] ,
9221040 suggestColumns : false ,
9231041 suggestTables : false ,
1042+ suggestScalarFunctions : false ,
1043+ suggestAggregateFunctions : false ,
1044+ suggestWindowFunctions : false ,
1045+ suggestTableValuedFunctions : false ,
9241046 referencedColumns : new Set ( ) ,
9251047 isConditionContext : false ,
9261048 }
@@ -951,12 +1073,20 @@ export function getContentAssist(
9511073 let nextTokenTypes : TokenType [ ] = [ ]
9521074 let suggestColumns = false
9531075 let suggestTables = false
1076+ let suggestScalarFunctions = false
1077+ let suggestAggregateFunctions = false
1078+ let suggestWindowFunctions = false
1079+ let suggestTableValuedFunctions = false
9541080 let isConditionContext = false
9551081 try {
9561082 const computed = computeSuggestions ( tokensForAssist )
9571083 nextTokenTypes = computed . nextTokenTypes
9581084 suggestColumns = computed . suggestColumns
9591085 suggestTables = computed . suggestTables
1086+ suggestScalarFunctions = computed . suggestScalarFunctions
1087+ suggestAggregateFunctions = computed . suggestAggregateFunctions
1088+ suggestWindowFunctions = computed . suggestWindowFunctions
1089+ suggestTableValuedFunctions = computed . suggestTableValuedFunctions
9601090 isConditionContext = computed . isConditionContext
9611091 } catch ( e ) {
9621092 // If content assist fails, return empty suggestions
@@ -1043,6 +1173,10 @@ export function getContentAssist(
10431173 qualifiedTableRef : qualifiedRef ?. table ,
10441174 suggestColumns,
10451175 suggestTables,
1176+ suggestScalarFunctions,
1177+ suggestAggregateFunctions,
1178+ suggestWindowFunctions,
1179+ suggestTableValuedFunctions,
10461180 referencedColumns,
10471181 isConditionContext,
10481182 }
0 commit comments