@@ -16,9 +16,9 @@ VectorSchemaRoot result = ArrowSql.execute(root,
1616```
1717
1818This 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` /
3636already 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
5462comparison_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
6073Full 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
66117Everything 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.
0 commit comments