@@ -128,15 +128,6 @@ pub fn frontend(
128128
129129 let ( binder, joins, leftover_where) = bind_from ( select, this_name, in_cols, statics) ?;
130130
131- let mut rel = Rel :: Scan ;
132- if let Some ( pred) = & leftover_where {
133- let pred = fold ( bool_context ( binder. expr ( pred) ?, "WHERE predicate" ) ?) ;
134- rel = Rel :: Filter {
135- input : Box :: new ( rel) ,
136- pred,
137- } ;
138- }
139-
140131 let mut out_cols = Vec :: new ( ) ;
141132 let mut exprs = Vec :: new ( ) ;
142133 let push_item = |out_cols : & mut Vec < Col > ,
@@ -162,12 +153,16 @@ pub fn frontend(
162153 default_name ( e) ,
163154 fold ( binder. expr ( e) ?) ,
164155 ) ?,
165- SelectItem :: ExprWithAlias { expr, alias } => push_item (
166- & mut out_cols,
167- & mut exprs,
168- alias. value . clone ( ) ,
169- fold ( binder. expr ( expr) ?) ,
170- ) ?,
156+ SelectItem :: ExprWithAlias { expr, alias } => {
157+ let e = fold ( binder. expr ( expr) ?) ;
158+ // Lateral aliases (wave-5 pins): later items and WHERE may
159+ // reference this alias; the real column still wins.
160+ binder
161+ . bound_aliases
162+ . borrow_mut ( )
163+ . push ( ( alias. value . clone ( ) , e. clone ( ) ) ) ;
164+ push_item ( & mut out_cols, & mut exprs, alias. value . clone ( ) , e) ?
165+ }
171166 SelectItem :: Wildcard ( opts) => {
172167 for ( name, e) in binder. expand_star ( None , opts) ? {
173168 push_item ( & mut out_cols, & mut exprs, name, e) ?;
@@ -192,6 +187,19 @@ pub fn frontend(
192187 }
193188 dedup_output_names ( & mut out_cols) ;
194189
190+ // WHERE binds AFTER the projection so DuckDB's lateral-alias extension
191+ // (an alias visible inside WHERE when no real column shares the name)
192+ // resolves; the plan shape is unchanged — Filter still sits under
193+ // Project on the scan.
194+ let mut rel = Rel :: Scan ;
195+ if let Some ( pred) = & leftover_where {
196+ let pred = fold ( bool_context ( binder. expr ( pred) ?, "WHERE predicate" ) ?) ;
197+ rel = Rel :: Filter {
198+ input : Box :: new ( rel) ,
199+ pred,
200+ } ;
201+ }
202+
195203 let named = out_cols
196204 . iter ( )
197205 . map ( |c| c. name . clone ( ) )
@@ -222,27 +230,57 @@ fn bind_from<'a>(
222230 let dyn_name = match & table. relation {
223231 TableFactor :: Table { name, alias, .. } => {
224232 let n = name. to_string ( ) ;
225- if !n. eq_ignore_ascii_case ( this_name) {
233+ // `main.tbl` resolves to bare `tbl` (DuckDB's default schema);
234+ // every OTHER qualifier fails in DuckDB itself with a
235+ // schema-does-not-exist Catalog Error, so it stays unsupported
236+ // here — never a silent bare-name fallback (wave-5 pins).
237+ let bare = n
238+ . strip_prefix ( "main." )
239+ . or_else ( || n. strip_prefix ( "MAIN." ) )
240+ . unwrap_or ( & n) ;
241+ if !bare. eq_ignore_ascii_case ( this_name) {
226242 return Err ( unsup ( format ! (
227243 "table '{n}' as the driving relation (must be the dynamic table '{this_name}')"
228244 ) ) ) ;
229245 }
246+ let n = bare. to_string ( ) ;
230247 match alias {
231248 // Measured: an alias REPLACES the original name entirely
232249 // (qualified refs through the original are binder errors in
233250 // DuckDB) — making the alias the binder's this_name gives
234251 // exactly that scoping.
235- Some ( a) if a. columns . is_empty ( ) => a. name . value . clone ( ) ,
236- Some ( _) => return Err ( unsup ( "column-renaming table alias t(a, b, ...)" ) ) ,
237- None => n,
252+ Some ( a) if a. columns . is_empty ( ) => ( a. name . value . clone ( ) , None ) ,
253+ // `t AS u(x, y)`: a PARTIAL list is legal (prefix rename,
254+ // remaining columns keep their names); too many names is
255+ // the pinned bind error; old names are fully shadowed
256+ // (wave-5 pins).
257+ Some ( a) => {
258+ if a. columns . len ( ) > in_cols. len ( ) {
259+ return Err ( PrepareError :: Bind ( format ! (
260+ "table \" {n}\" has {} columns available but {} columns specified" ,
261+ in_cols. len( ) ,
262+ a. columns. len( )
263+ ) ) ) ;
264+ }
265+ let mut renamed = in_cols. to_vec ( ) ;
266+ for ( c, def) in renamed. iter_mut ( ) . zip ( & a. columns ) {
267+ c. name = def. name . value . clone ( ) ;
268+ }
269+ ( a. name . value . clone ( ) , Some ( renamed) )
270+ }
271+ None => ( n, None ) ,
238272 }
239273 }
240274 other => return Err ( unsup ( format ! ( "FROM {other}" ) ) ) ,
241275 } ;
276+ let ( dyn_name, renamed_cols) = dyn_name;
242277
243278 let mut binder = Binder {
244279 this_name : dyn_name,
245- in_cols,
280+ in_cols : match renamed_cols {
281+ Some ( v) => std:: borrow:: Cow :: Owned ( v) ,
282+ None => std:: borrow:: Cow :: Borrowed ( in_cols) ,
283+ } ,
246284 joins : Vec :: new ( ) ,
247285 select_aliases : select
248286 . projection
@@ -252,6 +290,7 @@ fn bind_from<'a>(
252290 _ => None ,
253291 } )
254292 . collect ( ) ,
293+ bound_aliases : std:: cell:: RefCell :: new ( Vec :: new ( ) ) ,
255294 } ;
256295 let mut specs: Vec < JoinSpec > = Vec :: new ( ) ;
257296
@@ -328,7 +367,28 @@ fn bind_from<'a>(
328367 }
329368 ( keys, key_cols, Vec :: new ( ) , true )
330369 }
331- JoinConstraint :: Natural => return Err ( unsup ( "NATURAL JOIN" ) ) ,
370+ // NATURAL = USING(all common column names), case-insensitive,
371+ // merged output like USING with the LEFT spelling; NO common
372+ // columns is a hard error, never a cross product (wave-5 pins).
373+ JoinConstraint :: Natural => {
374+ let mut keys = Vec :: new ( ) ;
375+ let mut key_cols = Vec :: new ( ) ;
376+ for ( i, c) in st. cols . iter ( ) . enumerate ( ) {
377+ let Ok ( key) = binder. column ( & c. name ) else {
378+ continue ;
379+ } ;
380+ keys. push ( promote_key ( fold ( key) , st, i as u32 ) ?) ;
381+ key_cols. push ( i as u32 ) ;
382+ }
383+ if key_cols. is_empty ( ) {
384+ return Err ( PrepareError :: Bind (
385+ "No columns found to join on in NATURAL JOIN.\n \
386+ Use CROSS JOIN if you intended for this to be a cross-product."
387+ . into ( ) ,
388+ ) ) ;
389+ }
390+ ( keys, key_cols, Vec :: new ( ) , true )
391+ }
332392 JoinConstraint :: None => return Err ( unsup ( "JOIN without ON (cross join)" ) ) ,
333393 } ;
334394 let val_cols: Vec < u32 > = ( 0 ..st. cols . len ( ) as u32 )
@@ -768,11 +828,19 @@ struct ScopeJoin<'a> {
768828struct Binder < ' a > {
769829 /// The dynamic table's name as spelled in FROM.
770830 this_name : String ,
771- in_cols : & ' a [ Col ] ,
831+ /// The dynamic table's columns AS THE BINDER SEES THEM: borrowed
832+ /// normally; an owned renamed copy under `t AS u(x, y)` (wave-5 pins —
833+ /// prefix rename, old names fully shadowed). Positions never change,
834+ /// so the lowered program still marshals by the ORIGINAL field names.
835+ in_cols : std:: borrow:: Cow < ' a , [ Col ] > ,
772836 joins : Vec < ScopeJoin < ' a > > ,
773- /// SELECT-list aliases, for cleanly rejecting DuckDB's lateral-alias
774- /// extension (an alias referenced inside WHERE) as unsupported.
837+ /// All SELECT-list aliases (wave-5 pins: DuckDB's lateral aliases — a
838+ /// later item or WHERE may reference an earlier alias; the REAL column
839+ /// wins on a name clash; a forward reference is the pinned bind error).
775840 select_aliases : Vec < String > ,
841+ /// Aliases already bound this pass, in SELECT order (frontend() fills
842+ /// this as it walks the projection; RefCell keeps `expr(&self)` intact).
843+ bound_aliases : std:: cell:: RefCell < Vec < ( String , SExpr ) > > ,
776844}
777845
778846fn math1_node ( op : NumOp1 , inner : SExpr ) -> SExpr {
@@ -1014,15 +1082,64 @@ impl Binder<'_> {
10141082 }
10151083 }
10161084 }
1017- if any_num && any_other {
1018- return Err ( unsup (
1019- "BETWEEN/IN mixing strings or booleans with numbers (exec-time cast semantics)" ,
1020- ) ) ;
1085+ // Wave-5 pins: mixing casts the string/bool side to the NUMERIC
1086+ // side (strings numerically with half-away-from-zero rounding to
1087+ // ints; bool -> 0/1; non-numeric strings are DuckDB Conversion
1088+ // Errors). Only literals convert at bind — a string/bool COLUMN
1089+ // would need runtime cast traps and stays unsupported.
1090+ let mut owned: Vec < SqlExpr > = Vec :: with_capacity ( exprs. len ( ) ) ;
1091+ for e in exprs {
1092+ let b = self . expr_or_null ( e) ?;
1093+ let needs_cast =
1094+ any_num && b. as_ref ( ) . is_some_and ( |b| matches ! ( b. ty, Ty :: Str | Ty :: I1 ) ) ;
1095+ if !needs_cast {
1096+ owned. push ( ( * e) . clone ( ) ) ;
1097+ continue ;
1098+ }
1099+ let lit = match b. map ( |b| b. kind ) {
1100+ Some ( SKind :: Lit ( Lit :: Str ( s) ) ) => {
1101+ // Non-numeric strings convert (and fail) at EXECUTION
1102+ // time in DuckDB — an empty input succeeds — so a
1103+ // bind-time error would be over-eager; stay clean.
1104+ let Ok ( x) = s. trim ( ) . parse :: < f64 > ( ) else {
1105+ return Err ( unsup (
1106+ "BETWEEN/IN mixing non-numeric string literals with numbers \
1107+ (exec-time conversion)",
1108+ ) ) ;
1109+ } ;
1110+ if any_f64 {
1111+ s. trim ( ) . to_string ( )
1112+ } else {
1113+ let r = if x >= 0.0 {
1114+ ( x + 0.5 ) . floor ( )
1115+ } else {
1116+ ( x - 0.5 ) . ceil ( )
1117+ } ;
1118+ if r < i64:: MIN as f64 || r > i64:: MAX as f64 {
1119+ return Err ( unsup (
1120+ "BETWEEN/IN mixing out-of-range string literals with numbers \
1121+ (exec-time conversion)",
1122+ ) ) ;
1123+ }
1124+ format ! ( "{}" , r as i64 )
1125+ }
1126+ }
1127+ Some ( SKind :: Lit ( Lit :: I1 ( v) ) ) => format ! ( "{}" , v as i64 ) ,
1128+ _ => {
1129+ return Err ( unsup (
1130+ "BETWEEN/IN mixing strings or booleans with numbers \
1131+ (non-literal side needs exec-time cast semantics)",
1132+ ) )
1133+ }
1134+ } ;
1135+ owned. push ( SqlExpr :: Value ( sqlparser:: ast:: ValueWithSpan {
1136+ value : SqlValue :: Number ( lit, false ) ,
1137+ span : sqlparser:: tokenizer:: Span :: empty ( ) ,
1138+ } ) ) ;
10211139 }
1022- Ok ( exprs
1023- . iter ( )
1140+ Ok ( owned
1141+ . into_iter ( )
10241142 . map ( |e| {
1025- let e = ( * e) . clone ( ) ;
10261143 if any_f64 {
10271144 // CAST is a no-op on already-f64 sides and types NULL
10281145 // literals from context; exactly DuckDB's unification.
@@ -1796,23 +1913,37 @@ impl Binder<'_> {
17961913 }
17971914 }
17981915 match hits. len ( ) {
1916+ // The REAL column wins over a same-named select alias (measured
1917+ // in both SELECT and WHERE — wave-5 pins).
17991918 1 => Ok ( hits. pop ( ) . expect ( "len checked" ) ) ,
1800- // Real DuckDB features we don't model reject cleanly, not as
1801- // bind errors: the rowid pseudo-column, and DuckDB's lateral
1802- // alias extension (a SELECT alias visible inside WHERE).
18031919 0 if name. eq_ignore_ascii_case ( "rowid" ) => Err ( unsup ( "rowid pseudo-column" ) ) ,
1804- 0 if self
1805- . select_aliases
1806- . iter ( )
1807- . any ( |a| a. eq_ignore_ascii_case ( name) ) =>
1808- {
1809- Err ( unsup ( format ! (
1810- "SELECT alias '{name}' referenced outside the SELECT list (lateral alias)"
1920+ 0 => {
1921+ // Lateral aliases: an already-bound alias resolves to its
1922+ // expression; a known-but-later alias is the pinned
1923+ // forward-reference error.
1924+ if let Some ( ( _, e) ) = self
1925+ . bound_aliases
1926+ . borrow ( )
1927+ . iter ( )
1928+ . rev ( )
1929+ . find ( |( a, _) | a. eq_ignore_ascii_case ( name) )
1930+ {
1931+ return Ok ( e. clone ( ) ) ;
1932+ }
1933+ if self
1934+ . select_aliases
1935+ . iter ( )
1936+ . any ( |a| a. eq_ignore_ascii_case ( name) )
1937+ {
1938+ return Err ( PrepareError :: Bind ( format ! (
1939+ "column \" {name}\" referenced that exists in the SELECT clause - \
1940+ but this column cannot be referenced before it is defined"
1941+ ) ) ) ;
1942+ }
1943+ Err ( PrepareError :: Bind ( format ! (
1944+ "column '{name}' does not exist in scope"
18111945 ) ) )
18121946 }
1813- 0 => Err ( PrepareError :: Bind ( format ! (
1814- "column '{name}' does not exist in scope"
1815- ) ) ) ,
18161947 _ => Err ( PrepareError :: Bind ( format ! ( "ambiguous column '{name}'" ) ) ) ,
18171948 }
18181949 }
@@ -1893,7 +2024,18 @@ impl Binder<'_> {
18932024 let n = null_of ( null_context_ty ( op, b. ty ) ) ;
18942025 ( n, b)
18952026 }
1896- ( None , None ) => return Err ( unsup ( "NULL <op> NULL without a typing context" ) ) ,
2027+ ( None , None ) => {
2028+ // Wave-5 pins: NULL <op> NULL types by the operator —
2029+ // + - * % -> BIGINT, / -> DOUBLE, comparisons -> BOOLEAN
2030+ // (via I64 operands), AND/OR -> BOOLEAN.
2031+ let ty = match op {
2032+ BinaryOperator :: Divide => Ty :: F64 ,
2033+ BinaryOperator :: And | BinaryOperator :: Or => Ty :: I1 ,
2034+ BinaryOperator :: PGStartsWith | BinaryOperator :: StringConcat => Ty :: Str ,
2035+ _ => Ty :: I64 ,
2036+ } ;
2037+ ( null_of ( ty) , null_of ( ty) )
2038+ }
18972039 } ;
18982040 match op {
18992041 BinaryOperator :: Plus => self . arith ( ArithOp :: Add , a, b) ,
0 commit comments