@@ -916,7 +916,7 @@ async fn test_execute_statement(test_db: PgPool) -> Result<()> {
916916 . find_map ( |action_or_cmd| match action_or_cmd {
917917 lsp:: CodeActionOrCommand :: CodeAction ( code_action) => {
918918 let command = code_action. command . as_ref ( ) ;
919- if command. is_some_and ( |cmd| & cmd. command == "pgt .executeStatement" ) {
919+ if command. is_some_and ( |cmd| & cmd. command == "pgls .executeStatement" ) {
920920 let command = command. unwrap ( ) ;
921921 let arguments = command. arguments . as_ref ( ) . unwrap ( ) . clone ( ) ;
922922 Some ( ( command. command . clone ( ) , arguments) )
@@ -952,6 +952,160 @@ async fn test_execute_statement(test_db: PgPool) -> Result<()> {
952952 Ok ( ( ) )
953953}
954954
955+ #[ sqlx:: test( migrator = "pgls_test_utils::MIGRATIONS" ) ]
956+ async fn test_invalidate_schema_cache ( test_db : PgPool ) -> Result < ( ) > {
957+ let factory = ServerFactory :: default ( ) ;
958+ let mut fs = MemoryFileSystem :: default ( ) ;
959+
960+ let database = test_db
961+ . connect_options ( )
962+ . get_database ( )
963+ . unwrap ( )
964+ . to_string ( ) ;
965+ let host = test_db. connect_options ( ) . get_host ( ) . to_string ( ) ;
966+
967+ // Setup: Create a table with only id column (no name column yet)
968+ let setup = r#"
969+ create table public.users (
970+ id serial primary key
971+ );
972+ "# ;
973+
974+ test_db
975+ . execute ( setup)
976+ . await
977+ . expect ( "Failed to setup test database" ) ;
978+
979+ let mut conf = PartialConfiguration :: init ( ) ;
980+ conf. merge_with ( PartialConfiguration {
981+ db : Some ( PartialDatabaseConfiguration {
982+ database : Some ( database) ,
983+ host : Some ( host) ,
984+ ..Default :: default ( )
985+ } ) ,
986+ ..Default :: default ( )
987+ } ) ;
988+
989+ fs. insert (
990+ url ! ( "postgres-language-server.jsonc" )
991+ . to_file_path ( )
992+ . unwrap ( ) ,
993+ serde_json:: to_string_pretty ( & conf) . unwrap ( ) ,
994+ ) ;
995+
996+ let ( service, client) = factory
997+ . create_with_fs ( None , DynRef :: Owned ( Box :: new ( fs) ) )
998+ . into_inner ( ) ;
999+
1000+ let ( stream, sink) = client. split ( ) ;
1001+ let mut server = Server :: new ( service) ;
1002+
1003+ let ( sender, mut receiver) = channel ( CHANNEL_BUFFER_SIZE ) ;
1004+ let reader = tokio:: spawn ( client_handler ( stream, sink, sender) ) ;
1005+
1006+ server. initialize ( ) . await ?;
1007+ server. initialized ( ) . await ?;
1008+
1009+ server. load_configuration ( ) . await ?;
1010+
1011+ // Open a document that references a non-existent 'name' column
1012+ let doc_content = "select name from public.users;" ;
1013+ server. open_document ( doc_content) . await ?;
1014+
1015+ // Wait for typecheck diagnostics showing column doesn't exist
1016+ let got_error = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , async {
1017+ loop {
1018+ match receiver. next ( ) . await {
1019+ Some ( ServerNotification :: PublishDiagnostics ( msg) ) => {
1020+ if msg
1021+ . diagnostics
1022+ . iter ( )
1023+ . any ( |d| d. message . contains ( "column \" name\" does not exist" ) )
1024+ {
1025+ return true ;
1026+ }
1027+ }
1028+ _ => continue ,
1029+ }
1030+ }
1031+ } )
1032+ . await
1033+ . is_ok ( ) ;
1034+
1035+ assert ! (
1036+ got_error,
1037+ "Expected typecheck error for non-existent column 'name'"
1038+ ) ;
1039+
1040+ // Add the missing column to the database
1041+ let alter_table = r#"
1042+ alter table public.users
1043+ add column name text;
1044+ "# ;
1045+
1046+ test_db
1047+ . execute ( alter_table)
1048+ . await
1049+ . expect ( "Failed to add column to table" ) ;
1050+
1051+ // Invalidate the schema cache (all = false for current connection only)
1052+ server
1053+ . request :: < bool , ( ) > ( "pgt/invalidate_schema_cache" , "_invalidate_cache" , false )
1054+ . await ?;
1055+
1056+ // Change the document slightly to trigger re-analysis
1057+ server
1058+ . change_document (
1059+ 1 ,
1060+ vec ! [ TextDocumentContentChangeEvent {
1061+ range: Some ( Range {
1062+ start: Position {
1063+ line: 0 ,
1064+ character: 30 ,
1065+ } ,
1066+ end: Position {
1067+ line: 0 ,
1068+ character: 30 ,
1069+ } ,
1070+ } ) ,
1071+ range_length: Some ( 0 ) ,
1072+ text: " " . to_string( ) ,
1073+ } ] ,
1074+ )
1075+ . await ?;
1076+
1077+ // Wait for diagnostics to clear (no typecheck error anymore)
1078+ let error_cleared = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , async {
1079+ loop {
1080+ match receiver. next ( ) . await {
1081+ Some ( ServerNotification :: PublishDiagnostics ( msg) ) => {
1082+ // Check that there's no typecheck error for the column
1083+ let has_column_error = msg
1084+ . diagnostics
1085+ . iter ( )
1086+ . any ( |d| d. message . contains ( "column \" name\" does not exist" ) ) ;
1087+ if !has_column_error {
1088+ return true ;
1089+ }
1090+ }
1091+ _ => continue ,
1092+ }
1093+ }
1094+ } )
1095+ . await
1096+ . is_ok ( ) ;
1097+
1098+ assert ! (
1099+ error_cleared,
1100+ "Expected typecheck error to be cleared after schema cache invalidation"
1101+ ) ;
1102+
1103+ server. shutdown ( ) . await ?;
1104+ reader. abort ( ) ;
1105+
1106+ Ok ( ( ) )
1107+ }
1108+
9551109#[ sqlx:: test( migrator = "pgls_test_utils::MIGRATIONS" ) ]
9561110async fn test_issue_281 ( test_db : PgPool ) -> Result < ( ) > {
9571111 let factory = ServerFactory :: default ( ) ;
0 commit comments