66from __future__ import annotations
77
88import re
9+ from collections .abc import Sequence
10+
11+ from sqlit .domains .query .app .multi_statement import split_statements
912
1013from .alter_table import get_alter_table_completions
1114from .core import (
2730 is_inside_string ,
2831 remove_comments ,
2932 remove_string_literals ,
33+ split_identifier_parts ,
3034)
3135from .create_index import get_create_index_completions
3236from .create_table import get_create_table_completions
@@ -63,11 +67,60 @@ def get_context(sql: str, cursor_pos: int) -> list[Suggestion]:
6367 if before_cursor .rstrip ().endswith (";" ):
6468 return []
6569
70+ statements = split_statements (before_cursor )
71+ current_statement = statements [- 1 ] if statements else before_cursor
72+ routine_context = remove_comments (remove_string_literals (current_statement ))
73+ routine_context = re .split (
74+ r"\n(?=\s*(?:SELECT|INSERT|UPDATE|DELETE|MERGE|WITH|EXEC(?:UTE)?|"
75+ r"CREATE|ALTER|DROP)\b)" ,
76+ routine_context ,
77+ flags = re .IGNORECASE ,
78+ )[- 1 ]
79+ routine_invocation = re .search (
80+ r"\b(?:EXEC|EXECUTE)\s+"
81+ r"(?:@\w+\s*=\s*)?"
82+ r"([^\s,=;]+)"
83+ r"(?P<suffix>.*)$" ,
84+ routine_context ,
85+ re .IGNORECASE ,
86+ )
87+ if routine_invocation and routine_invocation .group ("suffix" ).startswith ((" " , "\t " , "\n " )):
88+ return [
89+ Suggestion (
90+ type = SuggestionType .PARAMETER ,
91+ table_scope = routine_invocation .group (1 ),
92+ )
93+ ]
94+ if routine_invocation and not routine_invocation .group ("suffix" ):
95+ target = routine_invocation .group (1 )
96+ parts = split_identifier_parts (target )
97+ if target .endswith ("." ):
98+ qualifiers = parts
99+ else :
100+ qualifiers = parts [:- 1 ]
101+ scope = "." .join (qualifiers ) if qualifiers else None
102+ return [Suggestion (type = SuggestionType .PROCEDURE , table_scope = scope )]
103+
104+ qualified_from = re .search (
105+ r"\b(?:FROM|JOIN|APPLY)\s+"
106+ r"(?:(?:\[[^\]]+\]|\w+)\.){1,2}\[?\w*$" ,
107+ before_cursor ,
108+ re .IGNORECASE ,
109+ )
110+ if qualified_from :
111+ return [Suggestion (type = SuggestionType .TABLE )]
112+
66113 # Check for table.column pattern (alias or table prefix)
67- dot_match = re .search (r"(\w+)\.\w*$" , before_cursor )
114+ dot_match = re .search (
115+ r"((?:\[[^\]]+\]|\w+)(?:\.(?:\[[^\]]+\]|\w+))*)\.\[?\w*$" ,
116+ before_cursor ,
117+ )
68118 if dot_match :
69119 prefix = dot_match .group (1 )
70- return [Suggestion (type = SuggestionType .ALIAS_COLUMN , table_scope = prefix )]
120+ return [
121+ Suggestion (type = SuggestionType .ALIAS_COLUMN , table_scope = prefix ),
122+ Suggestion (type = SuggestionType .FUNCTION , table_scope = prefix ),
123+ ]
71124
72125 # Try statement-specific handlers
73126 for handler in [get_insert_context , get_update_context , get_delete_context ]:
@@ -162,6 +215,77 @@ def get_completions(
162215 """
163216 before_cursor = sql [:cursor_pos ]
164217 current_word = get_current_word (sql , cursor_pos )
218+ routines = procedures or []
219+
220+ def routine_display_name (routine : object ) -> str :
221+ name = str (routine )
222+ identities = {
223+ (
224+ str (getattr (candidate , "database" , "" )).lower (),
225+ str (getattr (candidate , "schema" , "" )).lower (),
226+ )
227+ for candidate in routines
228+ if str (candidate ).lower () == name .lower ()
229+ }
230+ if len (identities ) <= 1 :
231+ return name
232+ parts = [
233+ str (getattr (routine , "database" , "" )),
234+ str (getattr (routine , "schema" , "" )),
235+ name ,
236+ ]
237+ return "." .join (part for part in parts if part )
238+
239+ def scoped_routine_names (
240+ candidates : Sequence [object ],
241+ * ,
242+ schema : str ,
243+ database : str = "" ,
244+ ) -> list [str ]:
245+ filtered = [
246+ routine
247+ for routine in candidates
248+ if str (getattr (routine , "schema" , "" )).lower () == schema .lower ()
249+ and (
250+ not database
251+ or str (getattr (routine , "database" , "" )).lower ()
252+ == database .lower ()
253+ )
254+ ]
255+ counts : dict [str , int ] = {}
256+ for routine in filtered :
257+ key = str (routine ).lower ()
258+ counts [key ] = counts .get (key , 0 ) + 1
259+ return [
260+ str (routine )
261+ for routine in filtered
262+ if counts [str (routine ).lower ()] == 1
263+ ]
264+
265+ procedure_names = [
266+ routine_display_name (routine )
267+ for routine in routines
268+ if getattr (routine , "routine_type" , "PROCEDURE" ) == "PROCEDURE"
269+ ]
270+ function_names = [
271+ routine_display_name (routine )
272+ for routine in routines
273+ if getattr (routine , "routine_type" , "" ) == "FUNCTION"
274+ ]
275+ legacy_routine_names = [
276+ str (routine ) for routine in routines if not hasattr (routine , "routine_type" )
277+ ]
278+ function_routines = [
279+ routine
280+ for routine in routines
281+ if getattr (routine , "routine_type" , "" ) == "FUNCTION"
282+ ]
283+ table_function_routines = [
284+ routine
285+ for routine in function_routines
286+ if bool (getattr (routine , "is_table_valued" , False ))
287+ ]
288+ table_function_names = [routine_display_name (routine ) for routine in table_function_routines ]
165289
166290 # Don't suggest if inside string literal
167291 if is_inside_string (before_cursor ):
@@ -183,7 +307,12 @@ def get_completions(
183307 return fuzzy_match (current_word , result )
184308
185309 # Try DROP handler (doesn't need columns)
186- drop_result = get_drop_completions (before_cursor , tables , procedures )
310+ drop_routines = (
311+ (function_names if include_functions else []) + legacy_routine_names
312+ if re .search (r"\bDROP\s+FUNCTION\b" , before_cursor , re .IGNORECASE )
313+ else procedure_names
314+ )
315+ drop_result = get_drop_completions (before_cursor , tables , drop_routines )
187316 if drop_result is not None :
188317 return fuzzy_match (current_word , drop_result )
189318
@@ -258,11 +387,27 @@ def get_completions(
258387
259388 # Schema.table prefix → suggest tables after schema name
260389 # Pattern: FROM/JOIN schema. or schema.partial
261- namespace_match = re .search (r"\b(?:FROM|JOIN)\s+(\w+)\.\w*$" , clean_before , re .IGNORECASE )
390+ namespace_match = re .search (
391+ r"\b(?:FROM|JOIN|APPLY)\s+"
392+ r"(?:(?:\[([^\]]+)\]|(\w+))\.)?"
393+ r"(?:\[([^\]]+)\]|(\w+))\.\[?\w*$" ,
394+ clean_before ,
395+ re .IGNORECASE ,
396+ )
262397 if namespace_match :
263- namespace = namespace_match .group (1 )
398+ database = namespace_match .group (1 ) or namespace_match .group (2 ) or ""
399+ namespace = namespace_match .group (3 ) or namespace_match .group (4 )
264400 names = get_names_for_namespace (namespace , tables )
265- return fuzzy_match (current_word , names if names else tables )
401+ candidates = list (names if names else tables )
402+ if include_functions :
403+ candidates .extend (
404+ scoped_routine_names (
405+ table_function_routines ,
406+ schema = namespace ,
407+ database = database ,
408+ )
409+ )
410+ return fuzzy_match (current_word , candidates )
266411
267412 # ANY/ALL/SOME ( → suggest SELECT for subquery
268413 if re .search (r"\b(ANY|ALL|SOME)\s*\(\s*\w*$" , clean_before , re .IGNORECASE ):
@@ -333,6 +478,8 @@ def get_completions(
333478 if suggestion .type == SuggestionType .TABLE :
334479 results .extend (get_identifier_namespaces (tables ))
335480 results .extend (tables )
481+ if include_functions :
482+ results .extend (table_function_names )
336483 results .extend (cte_names )
337484
338485 elif suggestion .type == SuggestionType .COLUMN :
@@ -358,6 +505,7 @@ def get_completions(
358505
359506 if include_functions :
360507 results .extend (get_all_functions ())
508+ results .extend (function_names )
361509
362510 elif suggestion .type == SuggestionType .ALIAS_COLUMN :
363511 scope = suggestion .table_scope
@@ -372,14 +520,74 @@ def get_completions(
372520 results .extend (columns [scope_lower ])
373521
374522 elif suggestion .type == SuggestionType .PROCEDURE :
375- if procedures :
376- results .extend (procedures )
523+ scope_parts = split_identifier_parts (suggestion .table_scope or "" )
524+ schema_scope = scope_parts [- 1 ].lower () if scope_parts else ""
525+ database_scope = scope_parts [- 2 ].lower () if len (scope_parts ) > 1 else ""
526+ if not schema_scope :
527+ results .extend (procedure_names )
528+ else :
529+ results .extend (
530+ scoped_routine_names (
531+ [
532+ routine
533+ for routine in routines
534+ if getattr (routine , "routine_type" , "PROCEDURE" )
535+ == "PROCEDURE"
536+ ],
537+ schema = schema_scope ,
538+ database = database_scope ,
539+ )
540+ )
541+
542+ elif suggestion .type == SuggestionType .FUNCTION :
543+ scope_parts = split_identifier_parts (suggestion .table_scope or "" )
544+ schema_scope = scope_parts [- 1 ].lower () if scope_parts else ""
545+ database_scope = scope_parts [- 2 ].lower () if len (scope_parts ) > 1 else ""
546+ is_alias = len (scope_parts ) == 1 and schema_scope in alias_map
547+ if include_functions and not is_alias :
548+ if schema_scope :
549+ results .extend (
550+ scoped_routine_names (
551+ function_routines ,
552+ schema = schema_scope ,
553+ database = database_scope ,
554+ )
555+ )
556+ else :
557+ results .extend (
558+ routine_display_name (routine )
559+ for routine in function_routines
560+ )
561+
562+ elif suggestion .type == SuggestionType .PARAMETER :
563+ parts = split_identifier_parts (suggestion .table_scope or "" )
564+ name_part = parts [- 1 ].lower () if parts else ""
565+ schema_part = parts [- 2 ].lower () if len (parts ) > 1 else ""
566+ database_part = parts [- 3 ].lower () if len (parts ) > 2 else ""
567+ matches = []
568+ for routine in routines :
569+ name = str (routine ).lower ()
570+ schema = str (getattr (routine , "schema" , "" )).lower ()
571+ database = str (getattr (routine , "database" , "" )).lower ()
572+ if (
573+ name == name_part
574+ and (not schema_part or schema == schema_part )
575+ and (not database_part or database == database_part )
576+ ):
577+ matches .append (routine )
578+ if not database_part and len (matches ) > 1 :
579+ # The user's default schema is not available here, so choosing
580+ # any same-named routine would risk suggesting invalid params.
581+ matches = []
582+ for routine in matches :
583+ results .extend (getattr (routine , "parameters" , ()))
377584
378585 elif suggestion .type == SuggestionType .KEYWORD :
379586 if include_keywords :
380587 results .extend (get_all_keywords ())
381588 if include_functions :
382589 results .extend (get_all_functions ())
590+ results .extend (function_names )
383591
384592 elif suggestion .type == SuggestionType .OPERATOR :
385593 results .extend (SQL_OPERATORS )
0 commit comments