Skip to content

Commit c9d1b35

Browse files
committed
fix: 5 operator bugs — TopK nulls-last, HashJoin NULL!=NULL, window sort, sentinel
- TopK null sort was nulls-first, inconsistent with rowComparator nulls-last - HashJoinOperator joined null keys (NULL=NULL) — now skips nulls in build/probe - WindowOperator partition sort put nulls first for DESC — now nulls-last always - SubqueryInOperator used "__null__" instead of NULL_SENTINEL - HashJoinOperator.toJoinKey used "__null__" instead of NULL_SENTINEL
1 parent 8558bb8 commit c9d1b35

1 file changed

Lines changed: 18 additions & 11 deletions

File tree

src/operators.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)