@@ -818,7 +818,7 @@ export class SubqueryInOperator implements Operator {
818818 const filtered : Row [ ] = [ ] ;
819819 for ( const row of batch ) {
820820 const val = row [ this . column ] ;
821- const key = val === null ? "__null__" : typeof val === "bigint" ? val . toString ( ) : String ( val ) ;
821+ const key = val === null || val === undefined ? NULL_SENTINEL : typeof val === "bigint" ? val . toString ( ) : String ( val ) ;
822822 if ( this . valueSet . has ( key ) ) {
823823 filtered . push ( row ) ;
824824 }
@@ -887,8 +887,8 @@ export class WindowOperator implements Operator {
887887 for ( const ob of win . orderBy ) {
888888 const av = rows [ a ] [ ob . column ] , bv = rows [ b ] [ ob . column ] ;
889889 if ( av === null && bv === null ) continue ;
890- if ( av === null ) return ob . direction === "asc" ? 1 : - 1 ;
891- if ( bv === null ) return ob . direction === "asc" ? - 1 : 1 ;
890+ if ( av === null ) return 1 ; // nulls-last regardless of direction
891+ if ( bv === null ) return - 1 ;
892892 if ( av < bv ) return ob . direction === "asc" ? - 1 : 1 ;
893893 if ( av > bv ) return ob . direction === "asc" ? 1 : - 1 ;
894894 }
@@ -1608,17 +1608,17 @@ export class TopKOperator implements Operator {
16081608
16091609 const cmp = ( a : Row , b : Row ) : number => {
16101610 const av = a [ col ] , bv = b [ col ] ;
1611- if ( av === null && bv === null ) return 0 ;
1612- if ( av === null ) return - 1 ;
1613- if ( bv === null ) return 1 ;
1611+ if ( ( av === null || av === undefined ) && ( bv === null || bv === undefined ) ) return 0 ;
1612+ if ( av === null || av === undefined ) return 1 ; // nulls-last
1613+ if ( bv === null || bv === undefined ) return - 1 ;
16141614 const c = av < bv ? - 1 : av > bv ? 1 : 0 ;
16151615 return desc ? - c : c ;
16161616 } ;
16171617
16181618 const shouldReplace = ( row : Row ) : boolean => {
16191619 const nv = row [ col ] , rv = heap [ 0 ] [ col ] ;
1620- if ( nv === null ) return false ;
1621- if ( rv === null ) return true ;
1620+ if ( nv === null || nv === undefined ) return false ;
1621+ if ( rv === null || rv === undefined ) return true ;
16221622 return desc ? nv > rv : nv < rv ;
16231623 } ;
16241624
@@ -2113,7 +2113,7 @@ export class HashJoinOperator implements Operator {
21132113 }
21142114
21152115 private toJoinKey ( val : Row [ string ] ) : string {
2116- if ( val === null ) return "__null__" ;
2116+ if ( val === null || val === undefined ) return NULL_SENTINEL ;
21172117 if ( typeof val === "bigint" ) return val . toString ( ) ;
21182118 return String ( val ) ;
21192119 }
@@ -2223,7 +2223,9 @@ export class HashJoinOperator implements Operator {
22232223 // Fits in memory — build hash map directly
22242224 this . hashMap = new Map < string , Row [ ] > ( ) ;
22252225 for ( const row of inMemoryRows ) {
2226- const key = this . toJoinKey ( row [ this . rightKey ] ) ;
2226+ const val = row [ this . rightKey ] ;
2227+ if ( val === null || val === undefined ) continue ; // NULL never matches in SQL joins
2228+ const key = this . toJoinKey ( val ) ;
22272229 const bucket = this . hashMap . get ( key ) ;
22282230 if ( bucket ) bucket . push ( row ) ;
22292231 else this . hashMap . set ( key , [ row ] ) ;
@@ -2457,7 +2459,12 @@ export class HashJoinOperator implements Operator {
24572459
24582460 const result : Row [ ] = [ ] ;
24592461 for ( const leftRow of batch ) {
2460- const key = this . toJoinKey ( leftRow [ this . leftKey ] ) ;
2462+ const leftVal = leftRow [ this . leftKey ] ;
2463+ if ( leftVal === null || leftVal === undefined ) {
2464+ if ( this . joinType === "left" || this . joinType === "full" ) result . push ( { ...leftRow } ) ;
2465+ continue ;
2466+ }
2467+ const key = this . toJoinKey ( leftVal ) ;
24612468 const rightRows = this . hashMap . get ( key ) ;
24622469 if ( rightRows ) {
24632470 for ( let i = 0 ; i < rightRows . length ; i ++ ) {
0 commit comments