@@ -103,6 +103,62 @@ impl SqlPlugin {
103103 }
104104 }
105105
106+ /// Build a qualifier→real-table map for the FROM/JOIN clauses of a query.
107+ /// Each real table maps to itself, and each alias (e.g. `u` in
108+ /// `FROM users u`) maps to its real table, so later passes can resolve a
109+ /// qualified reference like `u.id` to the `users` table.
110+ fn extract_table_aliases ( statement : & Statement ) -> Vec < ( String , String ) > {
111+ let mut aliases = Vec :: new ( ) ;
112+ match statement {
113+ Statement :: Query ( query) => {
114+ if let SetExpr :: Select ( select) = query. body . as_ref ( ) {
115+ for twj in & select. from {
116+ Self :: collect_aliases_from_join ( twj, & mut aliases) ;
117+ }
118+ }
119+ }
120+ Statement :: Delete ( delete) => match & delete. from {
121+ sqlparser:: ast:: FromTable :: WithFromKeyword ( twjs)
122+ | sqlparser:: ast:: FromTable :: WithoutKeyword ( twjs) => {
123+ for twj in twjs {
124+ Self :: collect_aliases_from_join ( twj, & mut aliases) ;
125+ }
126+ }
127+ } ,
128+ Statement :: Update { table, .. } => Self :: collect_aliases_from_join ( table, & mut aliases) ,
129+ _ => { }
130+ }
131+ aliases
132+ }
133+
134+ fn collect_aliases_from_join ( twj : & TableWithJoins , aliases : & mut Vec < ( String , String ) > ) {
135+ Self :: collect_alias_from_factor ( & twj. relation , aliases) ;
136+ for join in & twj. joins {
137+ Self :: collect_alias_from_factor ( & join. relation , aliases) ;
138+ }
139+ }
140+
141+ fn collect_alias_from_factor ( factor : & TableFactor , aliases : & mut Vec < ( String , String ) > ) {
142+ if let TableFactor :: Table { name, alias, .. } = factor {
143+ let real = name. to_string ( ) . to_lowercase ( ) ;
144+ aliases. push ( ( real. clone ( ) , real. clone ( ) ) ) ;
145+ if let Some ( alias) = alias {
146+ aliases. push ( ( alias. name . value . to_lowercase ( ) , real) ) ;
147+ }
148+ }
149+ }
150+
151+ /// Resolve a table qualifier (a real table name or an alias) to its real
152+ /// table name. An unknown qualifier falls back to itself, so a later schema
153+ /// lookup still flags it rather than silently passing.
154+ fn resolve_qualifier ( aliases : & [ ( String , String ) ] , qualifier : & str ) -> String {
155+ aliases
156+ . iter ( )
157+ . find ( |( q, _) | q == qualifier)
158+ . map ( |( _, t) | t. clone ( ) )
159+ . unwrap_or_else ( || qualifier. to_string ( ) )
160+ }
161+
106162 /// Extract all column references from a statement.
107163 fn extract_column_refs ( statement : & Statement ) -> Vec < ( Option < String > , String ) > {
108164 let mut cols = Vec :: new ( ) ;
@@ -172,12 +228,13 @@ impl SqlPlugin {
172228 right : & Expr ,
173229 schema : & Schema ,
174230 tables_in_query : & [ String ] ,
231+ aliases : & [ ( String , String ) ] ,
175232 ) -> Vec < TypeIssue > {
176233 let mut issues = Vec :: new ( ) ;
177234
178235 // Get types of left and right if we can resolve them
179- let left_type = Self :: infer_expr_type ( left, schema, tables_in_query) ;
180- let right_type = Self :: infer_expr_type ( right, schema, tables_in_query) ;
236+ let left_type = Self :: infer_expr_type ( left, schema, tables_in_query, aliases ) ;
237+ let right_type = Self :: infer_expr_type ( right, schema, tables_in_query, aliases ) ;
181238
182239 if let ( Some ( lt) , Some ( rt) ) = ( & left_type, & right_type) {
183240 let lt_cat = type_category ( lt) ;
@@ -225,7 +282,12 @@ impl SqlPlugin {
225282 }
226283
227284 /// Attempt to infer the SQL type of an expression given the schema.
228- fn infer_expr_type ( expr : & Expr , schema : & Schema , tables_in_query : & [ String ] ) -> Option < String > {
285+ fn infer_expr_type (
286+ expr : & Expr ,
287+ schema : & Schema ,
288+ tables_in_query : & [ String ] ,
289+ aliases : & [ ( String , String ) ] ,
290+ ) -> Option < String > {
229291 match expr {
230292 Expr :: Identifier ( ident) => {
231293 let col_name = ident. value . to_lowercase ( ) ;
@@ -240,7 +302,7 @@ impl SqlPlugin {
240302 None
241303 }
242304 Expr :: CompoundIdentifier ( parts) if parts. len ( ) == 2 => {
243- let table_name = parts[ 0 ] . value . to_lowercase ( ) ;
305+ let table_name = Self :: resolve_qualifier ( aliases , & parts[ 0 ] . value . to_lowercase ( ) ) ;
244306 let col_name = parts[ 1 ] . value . to_lowercase ( ) ;
245307 if let Some ( table) = schema. tables . iter ( ) . find ( |t| t. name == table_name)
246308 && let Some ( col) = table. columns . iter ( ) . find ( |c| c. name == col_name)
@@ -323,6 +385,7 @@ impl QueryLanguagePlugin for SqlPlugin {
323385 for stmt in & statements {
324386 // Check table references
325387 let table_refs = Self :: extract_table_refs ( stmt) ;
388+ let aliases = Self :: extract_table_aliases ( stmt) ;
326389 for table_name in & table_refs {
327390 if !schema. tables . iter ( ) . any ( |t| t. name == * table_name) {
328391 issues. push ( SchemaIssue {
@@ -334,10 +397,12 @@ impl QueryLanguagePlugin for SqlPlugin {
334397 // Check column references
335398 let col_refs = Self :: extract_column_refs ( stmt) ;
336399 for ( table_qualifier, col_name) in & col_refs {
337- let tables_to_check: Vec < & str > = if let Some ( tq) = table_qualifier {
338- vec ! [ tq. as_str( ) ]
400+ // Resolve an alias qualifier (`u`) to its real table (`users`);
401+ // an unqualified column is checked against every table in scope.
402+ let tables_to_check: Vec < String > = if let Some ( tq) = table_qualifier {
403+ vec ! [ Self :: resolve_qualifier( & aliases, tq) ]
339404 } else {
340- table_refs. iter ( ) . map ( |s| s . as_str ( ) ) . collect ( )
405+ table_refs. clone ( )
341406 } ;
342407
343408 let found = tables_to_check. iter ( ) . any ( |tn| {
@@ -378,13 +443,14 @@ impl QueryLanguagePlugin for SqlPlugin {
378443
379444 for stmt in & statements {
380445 let table_refs = Self :: extract_table_refs ( stmt) ;
446+ let aliases = Self :: extract_table_aliases ( stmt) ;
381447
382448 // Check WHERE clause binary operations for type compatibility
383449 if let Statement :: Query ( query) = stmt
384450 && let SetExpr :: Select ( select) = query. body . as_ref ( )
385451 && let Some ( ref selection) = select. selection
386452 {
387- Self :: check_expr_types ( selection, schema, & table_refs, & mut issues) ;
453+ Self :: check_expr_types ( selection, schema, & table_refs, & aliases , & mut issues) ;
388454 }
389455 }
390456
@@ -398,6 +464,7 @@ impl QueryLanguagePlugin for SqlPlugin {
398464
399465 for stmt in & statements {
400466 let table_refs = Self :: extract_table_refs ( stmt) ;
467+ let aliases = Self :: extract_table_aliases ( stmt) ;
401468
402469 // Check if SELECT includes nullable columns without COALESCE or IS NULL handling
403470 if let Statement :: Query ( q) = stmt
@@ -428,6 +495,29 @@ impl QueryLanguagePlugin for SqlPlugin {
428495 }
429496 }
430497 }
498+ // Alias-qualified projection (e.g. `u.email` in `FROM users u`):
499+ // resolve the qualifier so nullability is still checked.
500+ SelectItem :: UnnamedExpr ( Expr :: CompoundIdentifier ( parts) )
501+ | SelectItem :: ExprWithAlias {
502+ expr : Expr :: CompoundIdentifier ( parts) ,
503+ ..
504+ } if parts. len ( ) == 2 => {
505+ let table_name =
506+ Self :: resolve_qualifier ( & aliases, & parts[ 0 ] . value . to_lowercase ( ) ) ;
507+ let col_name = parts[ 1 ] . value . to_lowercase ( ) ;
508+ if let Some ( table) = schema. tables . iter ( ) . find ( |t| t. name == table_name)
509+ && let Some ( col) = table. columns . iter ( ) . find ( |c| c. name == col_name)
510+ && col. nullable
511+ {
512+ issues. push ( NullIssue {
513+ message : format ! (
514+ "Nullable column '{}' selected without COALESCE or null handling" ,
515+ col_name
516+ ) ,
517+ column : col_name. clone ( ) ,
518+ } ) ;
519+ }
520+ }
431521 _ => { }
432522 }
433523 }
@@ -444,16 +534,18 @@ impl SqlPlugin {
444534 expr : & Expr ,
445535 schema : & Schema ,
446536 tables : & [ String ] ,
537+ aliases : & [ ( String , String ) ] ,
447538 issues : & mut Vec < TypeIssue > ,
448539 ) {
449540 match expr {
450541 Expr :: BinaryOp { left, op, right } => {
451- let new_issues = Self :: check_binary_op_types ( op, left, right, schema, tables) ;
542+ let new_issues =
543+ Self :: check_binary_op_types ( op, left, right, schema, tables, aliases) ;
452544 issues. extend ( new_issues) ;
453- Self :: check_expr_types ( left, schema, tables, issues) ;
454- Self :: check_expr_types ( right, schema, tables, issues) ;
545+ Self :: check_expr_types ( left, schema, tables, aliases , issues) ;
546+ Self :: check_expr_types ( right, schema, tables, aliases , issues) ;
455547 }
456- Expr :: Nested ( inner) => Self :: check_expr_types ( inner, schema, tables, issues) ,
548+ Expr :: Nested ( inner) => Self :: check_expr_types ( inner, schema, tables, aliases , issues) ,
457549 _ => { }
458550 }
459551 }
0 commit comments