Skip to content

Commit 39f61b6

Browse files
committed
updated parser-ng-sql with GROUP BY, HAVING, CASE, WHEN, CAST, ORDER BY, LIMIT
1 parent 5fbf04b commit 39f61b6

11 files changed

Lines changed: 1485 additions & 128 deletions

File tree

‎parser-ng-sql/src/main/java/com/github/gbenroscience/sqlv1/ArrowQuery.java‎

Lines changed: 732 additions & 28 deletions
Large diffs are not rendered by default.

‎parser-ng-sql/src/main/java/com/github/gbenroscience/sqlv1/README.md‎

Lines changed: 118 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ VectorSchemaRoot result = ArrowSql.execute(root,
1616
```
1717

1818
This module intentionally stops at `SELECT`/`WHERE` with column aliasing —
19-
no `JOIN`, `INSERT`, `UPDATE`, `DELETE`, transactions, catalogs, subqueries,
20-
`GROUP BY`, or `ORDER BY`. The goal is *"make SQL a convenient way to
21-
describe vectorized Arrow computations"*, not *"build a database"*.
19+
no `JOIN`, `INSERT`, `UPDATE`, `DELETE`, transactions, catalogs, or
20+
subqueries. The goal is *"make SQL a convenient way to describe vectorized
21+
Arrow computations"*, not *"build a database"*.
2222

2323
## Installing into the ParserNG repo
2424

@@ -36,54 +36,119 @@ Requires JDK 22+ and network access to Maven Central for `arrow-vector` /
3636
already used by `parser-ng-arrow`.
3737

3838
## Grammar (v1)
39-
40-
```
41-
query ::= SELECT select_list FROM table_reference [WHERE boolean_expression]
42-
select_list ::= '*' | select_item (',' select_item)*
43-
select_item ::= expression [AS identifier]
44-
boolean_expression ::= or_expression
45-
or_expression ::= and_expression ('OR' and_expression)*
46-
and_expression ::= not_expression ('AND' not_expression)*
47-
not_expression ::= 'NOT' not_expression | predicate
48-
predicate ::= comparison
49-
| expression 'IS' ['NOT'] 'NULL'
50-
| expression ['NOT'] 'BETWEEN' expression 'AND' expression
51-
| expression ['NOT'] 'IN' '(' expression_list ')'
52-
| '(' boolean_expression ')'
53-
comparison ::= expression comparison_operator expression
39+
query ::= SELECT select_list
40+
FROM table_reference
41+
[WHERE boolean_expression]
42+
[GROUP BY expression_list]
43+
[HAVING boolean_expression]
44+
[ORDER BY order_item (',' order_item)]
45+
[LIMIT integer_literal]
46+
select_list ::= '' | select_item (',' select_item)*
47+
select_item ::= expression [AS identifier]
48+
| aggregate_call [AS identifier]
49+
aggregate_call ::= ('COUNT'|'SUM'|'AVG'|'MIN'|'MAX') '(' (expression|'') ')'
50+
-- '' only valid with COUNT
51+
order_item ::= expression ['ASC' | 'DESC']
52+
boolean_expression ::= or_expression
53+
or_expression ::= and_expression ('OR' and_expression)*
54+
and_expression ::= not_expression ('AND' not_expression)*
55+
not_expression ::= 'NOT' not_expression | predicate
56+
predicate ::= comparison
57+
| expression 'IS' ['NOT'] 'NULL'
58+
| expression ['NOT'] 'BETWEEN' expression 'AND' expression
59+
| expression ['NOT'] 'IN' '(' expression_list ')'
60+
| '(' boolean_expression ')'
61+
comparison ::= expression comparison_operator expression
5462
comparison_operator ::= '=' | '!=' | '<>' | '<' | '<=' | '>' | '>='
55-
expression ::= additive_expression (ParserNG's own arithmetic grammar --
56-
+ - * / % ^, parens, function calls,
57-
identifiers, literals)
58-
```
63+
expression ::= additive_expression (ParserNG's own arithmetic grammar --
64+
+ - * / % ^, parens, function calls,
65+
identifiers, literals, plus
66+
case_expression/cast_expression below)
67+
case_expression ::= 'CASE' expression ('WHEN' expression 'THEN' expression)+
68+
['ELSE' expression] 'END' -- simple CASE
69+
| 'CASE' ('WHEN' boolean_expression 'THEN' expression)+
70+
['ELSE' expression] 'END' -- searched CASE
71+
cast_expression ::= 'CAST' '(' expression 'AS' identifier ')'
5972

6073
Full javadoc for the grammar and its rationale lives on `SqlParser`; the
61-
`ast` package and `BoolExprs` document how `WHERE` is normalized (negation-
62-
normal form, since ParserNG has no logical-not operator) and rendered.
74+
`ast` package and `BoolExprs` document how `WHERE`/`HAVING` are normalized
75+
(negation-normal form, since ParserNG has no logical-not operator) and
76+
rendered.
77+
78+
### `CASE`/`WHEN`/`THEN`/`ELSE`/`END`
79+
80+
ParserNG has no `CASE` operator, but does have the `if(cond, then, else)`
81+
idiom described below. A `CASE` expression compiles to a right-nested chain
82+
of `if(...)` calls — `CASE WHEN a THEN 1 WHEN b THEN 2 ELSE 3 END` becomes
83+
`if(a, 1, if(b, 2, 3))`, and an omitted `ELSE` falls back to the literal
84+
`NULL`. Both the searched form (a full boolean condition per `WHEN`) and
85+
the simple form (`CASE x WHEN 1 THEN ... END`, compiled via `x == 1`) are
86+
supported. See `SqlParser`'s "CASE/WHEN/THEN/ELSE/END".
87+
88+
### `CAST`
89+
90+
`CAST(expr AS type)` has no ParserNG operator either, and since every value
91+
in this module is already a ParserNG/Arrow `float64`/`float32`, there is no
92+
representation change to make: `CAST(x AS INT/INTEGER/SMALLINT/BIGINT/LONG)`
93+
truncates toward zero (`if(x >= 0, floor(x), -1 * floor(-1 * (x)))` —
94+
assumes ParserNG provides `floor`); `CAST(x AS FLOAT/REAL/DOUBLE/DECIMAL/NUMERIC)`
95+
is a pure no-op. See `SqlParser`'s "CAST".
96+
97+
### `GROUP BY`/`HAVING`/aggregates and `ORDER BY`/`LIMIT`
98+
99+
`COUNT`/`SUM`/`AVG`/`MIN`/`MAX` are recognized only at the top level of a
100+
`select_item` (no nested aggregates, matching standard SQL). Every
101+
non-aggregate `select_item` in a grouped/aggregate query must match a
102+
`GROUP BY` expression verbatim — enforced by `SelectStatement`'s
103+
constructor. Grouping and aggregation are computed client-side (a plain
104+
nested-loop grouped aggregation, not a vectorized one — see `ArrowQuery`'s
105+
"Aggregation strategy"); `HAVING` then reuses `WHERE`'s own fused/leaf-mask
106+
predicate machinery against the resulting one-row-per-group data. Alias an
107+
aggregate item you intend to reference from `HAVING`/`ORDER BY`
108+
(`SUM(x) AS total ... HAVING total > 10`) rather than repeating its raw
109+
call text. `ORDER BY`/`LIMIT` apply after `WHERE`/`GROUP BY`/`HAVING` but
110+
before the final projection, so an `ORDER BY` key may also name a
111+
`SELECT`-list alias; sorting is a stable multi-key sort with `NULL`s last
112+
regardless of direction. See `ArrowQuery`'s "Aggregation strategy" and
113+
"`ORDER BY` and `LIMIT`".
63114

64115
## Package
65116

66117
Everything lives under `com.github.gbenroscience.sqlv1`.
67118

68119
## Verification status (please read before relying on this in production)
69120

70-
This module was written and reviewed in a sandboxed environment **without
71-
network access to Maven Central** and **without a JDK 22 installation** —
72-
only a JDK 21 runtime plus an apt-installable JDK 21 *compiler* were
73-
available, and only for packages already present in that environment.
74-
Concretely, that means two different levels of confidence for two different
75-
parts of this module:
121+
This module was originally written and reviewed in a sandboxed environment
122+
**without network access to Maven Central** but **with a JDK 21 compiler**
123+
available locally, letting the dependency-free part of it actually be
124+
compiled and run (see below). The `CASE`/`CAST`/`GROUP BY`/`HAVING`/
125+
`ORDER BY`/`LIMIT` additions described in this README were made in a
126+
follow-up sandbox with **no compiler available at all** (no `javac`, and no
127+
network access to install one) — those additions were reviewed by hand
128+
(balanced syntax, matching method signatures across files) but never
129+
compiled. Concretely, that means three different levels of confidence for
130+
three different parts of this module:
76131

77132
- **`SqlLexer`, `SqlParser`, the `ast` package, and `BoolExprs`** (SQL text
78133
→ AST → ParserNG expression text; no Arrow dependency at all) were
79134
**actually compiled and run** in that sandbox, against a real JDK 21
80135
`javac`/`java`, via the dependency-free harness
81-
`src/test/java/.../ParserSmokeTest.java`. All 38 of its checks pass,
82-
including the new embedded-boolean-condition cases
83-
(`if(sin(x) > 0, tan(x), 0.2)` and friends). The JUnit test class
84-
`SqlParserTest` (42 `@Test` methods) mirrors the same checks in ordinary
85-
JUnit 5 for this module's real build, since JUnit itself wasn't fetchable
86-
in that sandbox to run it there directly.
136+
`src/test/java/.../ParserSmokeTest.java`, for the original `SELECT`/
137+
`WHERE` grammar. All 38 of its checks passed, including the
138+
embedded-boolean-condition cases (`if(sin(x) > 0, tan(x), 0.2)` and
139+
friends). The JUnit test class `SqlParserTest` (42 `@Test` methods)
140+
mirrors the same checks in ordinary JUnit 5 for this module's real build,
141+
since JUnit itself wasn't fetchable in that sandbox to run it there
142+
directly.
143+
144+
**`CASE`/`WHEN`/`THEN`/`ELSE`/`END`, `CAST`, `GROUP BY`, `HAVING`,
145+
`ORDER BY`, and `LIMIT` were added afterward, in a sandbox with no
146+
compiler available at all (not even `javac`, let alone network access) —
147+
they were reviewed line-by-line against the existing code's own patterns
148+
and checked for balanced syntax and matching signatures across files, but
149+
have not been compiled or run anywhere.** Extend `ParserSmokeTest`/
150+
`SqlParserTest` to cover them, and run the full suite, before relying on
151+
these specific additions.
87152

88153
- **`ArrowQuery` and `ArrowSql`** (the classes that actually call into
89154
`arrow-vector` and `parser-ng-arrow`) could **not** be compiled in that
@@ -134,6 +199,22 @@ parts of this module:
134199
irrelevant to ParserNG's own whitespace-insensitive scanner, but worth
135200
knowing if you inspect `exprText()` directly rather than only its
136201
compiled/evaluated result.
202+
- **Grouped aggregation is client-side, not vectorized.** `GROUP BY`
203+
partitions rows and computes `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` with a plain
204+
nested loop (materializing each group's rows into their own small
205+
sub-batch) rather than a vectorized grouped-aggregation kernel — fine for
206+
parser-ng-sql's goal of a convenient computation language, not a claim of
207+
database-grade aggregation performance. See `ArrowQuery`'s "Aggregation
208+
strategy".
209+
- **`HAVING`/`ORDER BY` should reference an aggregate `SELECT` item by
210+
alias.** An unaliased aggregate item (`SUM(x)` with no `AS`) is named
211+
after its own reconstructed call text; referencing that same text from
212+
`HAVING`/`ORDER BY` only resolves if written identically. Always alias an
213+
aggregate you plan to reference from either clause.
214+
- **No nested aggregates and no `DISTINCT`.** `COUNT`/`SUM`/`AVG`/`MIN`/
215+
`MAX` are recognized only at the top level of a `select_item`, matching
216+
standard SQL's prohibition on nesting them; there is no `COUNT(DISTINCT ...)`
217+
or other `DISTINCT` support.
137218

138219
## Embedded boolean conditions (`if(sin(x) > 0, tan(x), 0.2)` and friends)
139220

@@ -161,5 +242,4 @@ intended restriction, and is now fixed:
161242
Nesting further inside a paren/argument those positions themselves open
162243
is still fully supported (`WHERE x = (y > 0)` works fine).
163244
- See `SqlParser`'s "Embedded boolean conditions" section for the full
164-
rationale and exactly which positions are safe.
165-
245+
rationale and exactly which positions are safe.

‎parser-ng-sql/src/main/java/com/github/gbenroscience/sqlv1/SqlLexer.java‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
* Quoted/bracketed identifiers ({@code "col"}, {@code [col]}, <code>`col`</code>)
1515
* are not supported in this v1; column and table names must be plain
1616
* identifiers, which also matches what ParserNG's own variable-name syntax
17-
* expects downstream.
17+
* expects downstream. {@code CAST}'s target type name ({@code INTEGER},
18+
* {@code DOUBLE}, etc.) is likewise lexed as a plain identifier — it is not
19+
* a reserved keyword, only meaningful in the {@code AS} position inside
20+
* {@code CAST(...)} (see {@link SqlParser}).
1821
*
1922
* <p>
2023
* Numbers are unsigned {@code integer_literal | decimal_literal} per the
@@ -32,11 +35,14 @@
3235
* <h2>Keywords</h2>
3336
* Keywords ({@code SELECT}, {@code FROM}, {@code WHERE}, {@code AS},
3437
* {@code AND}, {@code OR}, {@code NOT}, {@code BETWEEN}, {@code IN},
35-
* {@code IS}, {@code NULL}, {@code TRUE}, {@code FALSE}) are matched
36-
* case-insensitively, exactly like standard SQL; everything else
37-
* (identifiers, i.e. column/table/function names) is case-sensitive, since
38-
* those names are looked up verbatim against Arrow column names and
39-
* ParserNG's own case-sensitive function/variable names downstream.
38+
* {@code IS}, {@code NULL}, {@code TRUE}, {@code FALSE}, {@code CASE},
39+
* {@code WHEN}, {@code THEN}, {@code ELSE}, {@code END}, {@code CAST},
40+
* {@code GROUP}, {@code BY}, {@code HAVING}, {@code ORDER}, {@code ASC},
41+
* {@code DESC}, {@code LIMIT}) are matched case-insensitively, exactly like
42+
* standard SQL; everything else (identifiers, i.e. column/table/function
43+
* names, and a {@code CAST} target type name) is case-sensitive, since those
44+
* names are looked up verbatim against Arrow column names and ParserNG's
45+
* own case-sensitive function/variable names downstream.
4046
*
4147
* @author GBEMIRO
4248
*/
@@ -55,7 +61,20 @@ public final class SqlLexer {
5561
Map.entry("IS", TokenType.IS),
5662
Map.entry("NULL", TokenType.NULL),
5763
Map.entry("TRUE", TokenType.TRUE),
58-
Map.entry("FALSE", TokenType.FALSE)
64+
Map.entry("FALSE", TokenType.FALSE),
65+
Map.entry("CASE", TokenType.CASE),
66+
Map.entry("WHEN", TokenType.WHEN),
67+
Map.entry("THEN", TokenType.THEN),
68+
Map.entry("ELSE", TokenType.ELSE),
69+
Map.entry("END", TokenType.END),
70+
Map.entry("CAST", TokenType.CAST),
71+
Map.entry("GROUP", TokenType.GROUP),
72+
Map.entry("BY", TokenType.BY),
73+
Map.entry("HAVING", TokenType.HAVING),
74+
Map.entry("ORDER", TokenType.ORDER),
75+
Map.entry("ASC", TokenType.ASC),
76+
Map.entry("DESC", TokenType.DESC),
77+
Map.entry("LIMIT", TokenType.LIMIT)
5978
);
6079

6180
private final String src;
@@ -237,4 +256,4 @@ private Token string(int start, char quote) {
237256
}
238257
return new Token(TokenType.STRING, sb.toString(), start, pos);
239258
}
240-
}
259+
}

0 commit comments

Comments
 (0)