Skip to content

Commit 33c8fdd

Browse files
committed
fix: 8 bugs — scientific notation parse, SQL comments, NOT IN NULL, frame bounds
SQL parser/lexer: - 1e10 parsed via parseInt gave 1 instead of 10000000000 — now uses parseFloat - SQL line comments (--) and block comments (/* */) now skipped by lexer - 1e (no digits after exponent) no longer consumed as valid number - Window frame default end was "current" instead of "current_row" (NaN frame bound) - Hoisted comparison opMap to module scope (no per-parse allocation) - Removed dead variable colTok in table.column parsing SQL evaluator: - NOT IN with NULL elements now returns NULL per SQL three-valued logic Operators: - Cross join push(...batch) → loop to prevent stack overflow Types/partial-agg: - rowComparator handles undefined (missing keys after joins) - GROUP BY key restoration guards against Infinity/NaN strings
1 parent 467cc0f commit 33c8fdd

File tree

6 files changed

+42
-18
lines changed

6 files changed

+42
-18
lines changed

src/operators.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2166,7 +2166,7 @@ export class HashJoinOperator implements Operator {
21662166
while (true) {
21672167
const batch = await this.right.next();
21682168
if (!batch) break;
2169-
this.crossRightBuffer.push(...batch);
2169+
for (const row of batch) this.crossRightBuffer.push(row);
21702170
}
21712171
return;
21722172
}

src/partial-agg.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export function finalizePartialAgg(
295295
} else {
296296
// Attempt to restore numeric types
297297
const num = Number(part);
298-
row[groupCols[i]] = part !== "" && !isNaN(num) && String(num) === part ? num : part;
298+
row[groupCols[i]] = part !== "" && !isNaN(num) && isFinite(num) && String(num) === part ? num : part;
299299
}
300300
}
301301
for (let i = 0; i < states.length; i++) {

src/sql/evaluator.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ export function evaluateExpr(expr: SqlExpr, row: Row): unknown {
3030
const val = evaluateExpr(expr.expr, row);
3131
if (val === null) return null;
3232
let found = false;
33+
let hasNull = false;
3334
for (const v of expr.values) {
34-
if (looseEqual(val, evaluateExpr(v, row))) { found = true; break; }
35+
const ev = evaluateExpr(v, row);
36+
if (ev === null) { hasNull = true; continue; }
37+
if (looseEqual(val, ev)) { found = true; break; }
3538
}
36-
return expr.negated ? !found : found;
39+
if (found) return !expr.negated;
40+
if (hasNull) return null; // SQL three-valued: NOT IN (..., NULL) → NULL when not found
41+
return expr.negated;
3742
}
3843

3944
case "between": {

src/sql/lexer.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ export function tokenize(sql: string): Token[] {
126126
const start = pos;
127127
const ch = sql[pos];
128128

129+
// Line comments (--)
130+
if (ch === "-" && pos + 1 < len && sql[pos + 1] === "-") {
131+
while (pos < len && sql[pos] !== "\n") pos++;
132+
continue;
133+
}
134+
// Block comments (/* */)
135+
if (ch === "/" && pos + 1 < len && sql[pos + 1] === "*") {
136+
pos += 2;
137+
while (pos + 1 < len && !(sql[pos] === "*" && sql[pos + 1] === "/")) pos++;
138+
pos += 2;
139+
continue;
140+
}
141+
129142
// Identifiers and keywords
130143
if (/[a-zA-Z_]/.test(ch)) {
131144
while (pos < len && /[a-zA-Z0-9_]/.test(sql[pos])) pos++;
@@ -143,9 +156,14 @@ export function tokenize(sql: string): Token[] {
143156
while (pos < len && /[0-9]/.test(sql[pos])) pos++;
144157
}
145158
if (pos < len && (sql[pos] === "e" || sql[pos] === "E")) {
159+
const ePos = pos;
146160
pos++;
147161
if (pos < len && (sql[pos] === "+" || sql[pos] === "-")) pos++;
148-
while (pos < len && /[0-9]/.test(sql[pos])) pos++;
162+
if (pos < len && /[0-9]/.test(sql[pos])) {
163+
while (pos < len && /[0-9]/.test(sql[pos])) pos++;
164+
} else {
165+
pos = ePos; // rollback — 'e' is not part of this number
166+
}
149167
}
150168
tokens.push({ type: TokenType.NUMBER, lexeme: sql.slice(start, pos), position: start });
151169
continue;

src/sql/parser.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import type {
88
SqlStatement, ShowVersionsStmt, DiffStmt,
99
} from "./ast.js";
1010

11+
const COMPARISON_OPS: readonly [TokenType, BinaryOp][] = [
12+
[TokenType.EQ, "eq"], [TokenType.NE, "ne"],
13+
[TokenType.LT, "lt"], [TokenType.LE, "le"],
14+
[TokenType.GT, "gt"], [TokenType.GE, "ge"],
15+
];
16+
1117
export function parse(sql: string): SelectStmt {
1218
const tokens = tokenize(sql);
1319
const parser = new Parser(tokens, sql);
@@ -498,12 +504,7 @@ class Parser {
498504
}
499505

500506
// Standard comparisons
501-
const opMap: [TokenType, BinaryOp][] = [
502-
[TokenType.EQ, "eq"], [TokenType.NE, "ne"],
503-
[TokenType.LT, "lt"], [TokenType.LE, "le"],
504-
[TokenType.GT, "gt"], [TokenType.GE, "ge"],
505-
];
506-
for (const [tt, op] of opMap) {
507+
for (const [tt, op] of COMPARISON_OPS) {
507508
if (this.match(tt)) {
508509
const right = this.parseAddSub();
509510
return { kind: "binary", op, left, right };
@@ -603,8 +604,9 @@ class Parser {
603604

604605
// Number
605606
if (this.match(TokenType.NUMBER)) {
606-
const num = tok.lexeme.includes(".") ? parseFloat(tok.lexeme) : parseInt(tok.lexeme, 10);
607-
const valType = tok.lexeme.includes(".") ? "float" as const : "integer" as const;
607+
const isFloat = tok.lexeme.includes(".") || tok.lexeme.includes("e") || tok.lexeme.includes("E");
608+
const num = isFloat ? parseFloat(tok.lexeme) : parseInt(tok.lexeme, 10);
609+
const valType = isFloat ? "float" as const : "integer" as const;
608610
return { kind: "value", value: { type: valType, value: num } };
609611
}
610612

@@ -663,7 +665,6 @@ class Parser {
663665

664666
// Check for table.column
665667
if (this.match(TokenType.DOT)) {
666-
const colTok = this.current();
667668
if (this.match(TokenType.STAR)) {
668669
// table.* in expression context
669670
return { kind: "column", table: name, name: "*" };
@@ -734,7 +735,7 @@ class Parser {
734735
const frameType = this.match(TokenType.ROWS) ? "rows" as const : (this.advance(), "range" as const);
735736
this.match(TokenType.BETWEEN); // consume optional BETWEEN keyword
736737
const start = this.parseFrameBound();
737-
let end: NonNullable<SqlWindowSpec["frame"]>["end"] = { type: "current" as const };
738+
let end: NonNullable<SqlWindowSpec["frame"]>["end"] = { type: "current_row" as const };
738739
if (this.match(TokenType.AND)) {
739740
end = this.parseFrameBound();
740741
}

src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export function rowComparator(col: string, desc: boolean): (a: Row, b: Row) => n
1717
const dir = desc ? -1 : 1;
1818
return (a: Row, b: Row): number => {
1919
const av = a[col], bv = b[col];
20-
if (av === null && bv === null) return 0;
21-
if (av === null) return 1;
22-
if (bv === null) return -1;
20+
if ((av === null || av === undefined) && (bv === null || bv === undefined)) return 0;
21+
if (av === null || av === undefined) return 1;
22+
if (bv === null || bv === undefined) return -1;
2323
return av < bv ? -dir : av > bv ? dir : 0;
2424
};
2525
}

0 commit comments

Comments
 (0)