Skip to content

Commit 5216fce

Browse files
committed
feat: expand native PostgreSQL expressions, queries, DML and DDL
Replace shallow handling of common PostgreSQL statements with validated, traversable AST productions. Keep all production parsing inside ParserSQL; libpg_query remains a test oracle, with no runtime dependency or fallback. Add EXTRACT, SQL SUBSTRING/TRIM, time-zone expressions, interval qualifiers, qualified operators, expression-valued type modifiers, ARRAY subqueries, IS DISTINCT FROM, quantified comparison operands and NORMALIZE. Preserve special datetime precision and MERGE_ACTION grammar while rejecting reserved function names and malformed delimiters or wildcard operands. Add grouping sets, ROLLUP/CUBE, grouping quantifiers, OFFSET/LIMIT ordering, FETCH, table-function ordinality and typed output columns. Preserve ONLY and explicit inheritance sources, nested WITH operands, quoted names and CASE branches in reconstructed SQL. Enforce restricted FETCH/OFFSET ROWS operands. Parse MERGE actions and RETURNING, modifying CTE bodies and main statements, SEARCH/CYCLE, INSERT OVERRIDING and compound sources, assignment indirection, and RETURNING options. Guard unsupported local DML/query semantics before execution and propagate arena exhaustion instead of dropping syntax. Add structured common table/schema/database creation, ALTER TABLE, DROP, index/view/function/procedure/trigger definitions, privileges, TRUNCATE and maintenance statements. Add explicit SQL/JSON and XML constructors, query functions, aggregates and table columns, including behavior/default clauses. Unsupported variants remain errors or incomplete input, not opaque AST tails. Append statement/node kinds without changing the existing AST layout. Update compatibility type mappings, focused positive/negative/AST tests, allocation regressions and coverage documentation. Leave benchmark code/results and benchmark-only README/.gitignore edits outside this commit. Validation: force-rebuilt full GoogleTest suite and corpus runner, focused ASan/UBSan grammar tests, PostgreSQL 18.4 parse/emit AST comparisons, malformed input mutation checks and a replay of the pinned 51,415-statement corpus.
1 parent de06d72 commit 5216fce

30 files changed

Lines changed: 4096 additions & 83 deletions

‎Makefile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
5353
$(TEST_DIR)/test_compound.cpp \
5454
$(TEST_DIR)/test_pg_query_features.cpp \
5555
$(TEST_DIR)/test_pg_expressions.cpp \
56+
$(TEST_DIR)/test_pg_special_expressions.cpp \
57+
$(TEST_DIR)/test_pg_query_gaps.cpp \
58+
$(TEST_DIR)/test_pg_dml_extensions.cpp \
59+
$(TEST_DIR)/test_pg_ddl.cpp \
60+
$(TEST_DIR)/test_pg_json_xml.cpp \
5661
$(TEST_DIR)/test_pg_utilities.cpp \
5762
$(TEST_DIR)/test_parse_all.cpp \
5863
$(TEST_DIR)/test_ast_utilities.cpp \

‎docs/postgresql-analysis-features.md‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ validation and other semantic checks.
2323

2424
The new clauses remain bounded by the existing expression grammar. PostgreSQL
2525
subqueries now share compound-query parsing, including SELECT, TABLE, VALUES and
26-
WITH operands in derived tables and LATERAL. Interval-valued frame offsets remain
27-
outside the supported expression subset.
26+
WITH operands in derived tables and LATERAL. Interval-valued frame offsets are supported through structured interval literals.
2827

2928
Window expressions previously disappeared from emitted SQL and digests because
3029
their AST nodes had no emitter handlers. They now appear correctly. Digest
@@ -66,11 +65,10 @@ names, operators or function overloads, and does not validate catalog semantics.
6665
Consumers must also handle the appended node/token kinds. `?` is a PostgreSQL
6766
operator; MySQL continues to tokenize it as an anonymous bind marker.
6867

69-
This phase does not cover qualified `OPERATOR(schema.op)` syntax, arbitrary
70-
expression-valued type modifiers, every keyword in an unquoted type-name
71-
position, interval prefix-literal qualifiers, or the remaining full expression
72-
and DDL grammar. These additions improve the native ParserSQL implementation;
73-
there is no PostgreSQL runtime fallback or production library dependency.
68+
Qualified prefix and binary `OPERATOR(schema.op)` syntax, expression-valued
69+
type modifiers and interval prefix-literal qualifiers are also supported.
70+
These additions improve the native ParserSQL implementation; there is no
71+
PostgreSQL runtime fallback or production library dependency.
7472

7573
## Qualified calls, aggregates and CTEs
7674

@@ -99,12 +97,57 @@ recursive/nested CTEs, CTE output-column lists, materialization hints and
9997
VALUES/TABLE query bodies before CTE materialization. These additions do not extend CTE parameterization, which
10098
continues to return an unsupported-context error.
10199

102-
Remaining gaps include data-modifying CTEs and main statements, SEARCH/CYCLE,
103-
special EXTRACT/SUBSTRING forms, AT TIME ZONE, advanced grouping, and broader
104-
SQL/JSON, XML and DDL grammar. See PostgreSQL's
100+
See PostgreSQL's
105101
[aggregate syntax](https://www.postgresql.org/docs/18/sql-expressions.html#SYNTAX-AGGREGATES)
106102
and [WITH queries](https://www.postgresql.org/docs/18/queries-with.html).
107103

104+
## Further native grammar coverage
105+
106+
Common PostgreSQL expressions now include `EXTRACT`, SQL `SUBSTRING`, `TRIM`,
107+
`AT TIME ZONE`, `AT LOCAL`, interval literal precision/field qualifiers,
108+
`IS [NOT] DISTINCT FROM`, `ARRAY(query)`, quantified comparison operands
109+
(`ANY`/`ALL`/`SOME`) and Unicode `NORMALIZE`. Operands remain AST children.
110+
Qualified operators and syntax modifiers retain their spelling during emission.
111+
112+
Queries support `GROUPING SETS`, `ROLLUP`, `CUBE`, grouping `ALL`/`DISTINCT`,
113+
`LIMIT ALL`, either LIMIT/OFFSET order, offset-only pagination and
114+
`FETCH FIRST/NEXT ... ROWS ONLY/WITH TIES`. Table functions support
115+
`WITH ORDINALITY` and typed output-column definitions. `ONLY` relation sources
116+
and parenthesized WITH operands are retained. New constructs that the local
117+
engine cannot execute are rejected by its planner.
118+
119+
`MERGE` has structured target/source, join condition, match branches, optional
120+
conditions, UPDATE/DELETE/INSERT/DO NOTHING actions and RETURNING. CTE bodies
121+
and main statements may use supported INSERT/UPDATE/DELETE/MERGE forms.
122+
Recursive `SEARCH` and `CYCLE` clauses, INSERT `OVERRIDING`, compound query
123+
sources, assignment indirection and RETURNING options retain their structure.
124+
The local DML planner rejects these new semantics before execution.
125+
126+
Common DDL now has validated productions for CREATE TABLE (including CTAS and
127+
partition bounds), basic CREATE SCHEMA/DATABASE, ALTER TABLE actions, DROP
128+
families, indexes,
129+
views/materialized views, functions/procedures, triggers, privileges, role
130+
membership, TRUNCATE, VACUUM and ANALYZE. Names, expressions, lists and clauses
131+
are traversable; type names and fixed syntax are validated leaves. Quoted
132+
function bodies remain string literals. This is syntax parsing, not catalog
133+
validation or execution, and unsupported options must leave an error or
134+
incomplete input rather than being accepted as an arbitrary tail.
135+
136+
SQL/JSON and XML productions retain constructors, JSON_ARRAYAGG/JSON_OBJECTAGG,
137+
query/value/exists calls,
138+
RETURNING and behavior options, table columns, serialization and default
139+
expressions. Their new node kinds support traversal and SQL reconstruction;
140+
parameterization remains conservative about unsupported contexts.
141+
142+
Coverage is still a subset of PostgreSQL. Remaining examples include CREATE
143+
TYPE/DOMAIN/SEQUENCE, embedded CREATE SCHEMA elements, CREATE DATABASE options,
144+
most non-table ALTER commands, unquoted SQL function bodies, exclusion
145+
constraints, less common DDL options, assignment slices,
146+
full ON CONFLICT index-inference options, and `IS JSON` predicates. PostgreSQL remains responsible
147+
for name resolution, types, privileges and semantic checks. Corpus acceptance
148+
alone does not establish grammar parity or round-trip equivalence for every
149+
accepted statement.
150+
108151
## Multiple statements
109152

110153
```cpp
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Remaining PostgreSQL grammar gaps
2+
3+
The user authorized filling the remaining gaps in ParserSQL's own grammar.
4+
Continue on feat/postgresql-coverage-and-ast-apis and update PR #67. Keep detailed
5+
commits and leave all benchmark work uncommitted. Baseline: de06d72, 27,014 of
6+
51,415 pinned PostgreSQL 18.4 statements completely parsed.
7+
8+
## Implementation groups
9+
10+
1. Expressions/types: special EXTRACT and SUBSTRING syntax, AT TIME ZONE,
11+
interval literals and qualifiers, qualified OPERATOR and type modifiers.
12+
Preserve structural operands and source spelling; verify precedence,
13+
malformed input and equivalent PostgreSQL ASTs after SQL emission.
14+
2. Query clauses: GROUPING SETS/ROLLUP/CUBE, both LIMIT/OFFSET orders and FETCH,
15+
WITH ORDINALITY and typed table-function output columns. Expressions remain
16+
AST children; syntax modifiers must not become bind parameters.
17+
3. DML/CTEs: MERGE branches/actions/RETURNING, WITH preceding DML and modifying
18+
CTE bodies, SEARCH/CYCLE, and remaining practical INSERT variants. Distinguish
19+
query subexpressions from DML bodies; reject unsupported local execution.
20+
4. DDL/utility: structure common CREATE/ALTER/DROP tables, views, functions,
21+
indexes and triggers, privilege and maintenance forms. Do not claim complete
22+
parsing by storing an unvalidated tail or skipping arbitrary tokens.
23+
5. SQL/JSON and XML: add explicit productions for structured constructors,
24+
query/value functions, JSON_TABLE/XMLTABLE and serialization options. Keep
25+
default expressions traversable and preserve server-defined syntax.
26+
6. Integration: test first in each group, obtain independent review, force the
27+
full C++ rebuild, run sanitizers and pinned oracle AST comparisons, inspect
28+
every compatibility transition, refresh correctness fixtures, then commit
29+
and publish the PR update. Report remaining variants honestly.
30+
31+
## Coordination and review
32+
33+
Use the subagent-development skill for independent grammar implementations.
34+
Each group owns its parser headers and tests. Shared AST/emitter/planner edits
35+
must be confined to distinct marked sections; no global rewrites. The parent
36+
owns integration, query clauses and final corpus validation. Existing MySQL
37+
behavior and the 48-byte AST layout remain unchanged. Append node/statement
38+
kinds. No production PostgreSQL dependency or parser fallback.
39+
40+
Review focus: syntax rejection versus semantic validation; SQL emission
41+
preserving names, precedence and options; valid nesting; parameterization
42+
contexts; local execution refusing unsupported semantics before side effects.
43+
Full PostgreSQL grammar parity must not be claimed from corpus counts alone.
44+
45+
## Completed verification
46+
47+
Implemented all five grammar groups with focused malformed-input and AST tests.
48+
Independent reviews covered DML/DDL and expressions/query clauses; confirmed
49+
findings were fixed and rechecked. ASan/UBSan passes all 47 new grammar tests,
50+
including arena-exhaustion regressions. The forced full rebuild passes 1,428
51+
tests with 37 backend-dependent tests skipped. The compatibility harness passes
52+
87 tests, and 215 representative PostgreSQL 18.4 AST round trips match after
53+
removing source-position fields.
54+
55+
Pinned corpus replay: 43,373/51,415 deeply parsed, versus 27,014 before this
56+
phase; zero baseline regressions. All 1,274 former classification-only cases
57+
now have complete ASTs. Reviewed 5,919 changes between unsupported categories:
58+
5,165 trailing-input to error, 723 type-mismatch to error, and 31 trailing-input
59+
to partial. These reflect stricter productions rather than lost accepted SQL;
60+
unsupported CREATE/ALTER and expression variants remain documented limitations.
61+
The generated correctness snapshot is refreshed separately after the code commit
62+
so its parser revision identifies the implementation being measured.

‎include/sql_engine/dml_plan_builder.h‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "sql_engine/plan_node.h"
1010
#include "sql_engine/catalog.h"
11+
#include "sql_engine/plan_builder.h"
1112
#include "sql_parser/ast.h"
1213
#include "sql_parser/common.h"
1314
#include "sql_parser/arena.h"
@@ -23,7 +24,7 @@ class DmlPlanBuilder {
2324
: catalog_(catalog), arena_(arena) {}
2425

2526
PlanNode* build(const sql_parser::AstNode* stmt_ast) {
26-
if (!stmt_ast) return nullptr;
27+
if (!supported(stmt_ast)) return nullptr;
2728
switch (stmt_ast->type) {
2829
case sql_parser::NodeType::NODE_INSERT_STMT:
2930
return build_insert(stmt_ast);
@@ -37,7 +38,7 @@ class DmlPlanBuilder {
3738
}
3839

3940
PlanNode* build_insert(const sql_parser::AstNode* insert_ast) {
40-
if (!insert_ast) return nullptr;
41+
if (!supported(insert_ast)) return nullptr;
4142

4243
PlanNode* node = make_plan_node(arena_, PlanNodeType::INSERT_PLAN);
4344
if (!node) return nullptr;
@@ -104,7 +105,7 @@ class DmlPlanBuilder {
104105
}
105106

106107
PlanNode* build_update(const sql_parser::AstNode* update_ast) {
107-
if (!update_ast) return nullptr;
108+
if (!supported(update_ast)) return nullptr;
108109

109110
PlanNode* node = make_plan_node(arena_, PlanNodeType::UPDATE_PLAN);
110111
if (!node) return nullptr;
@@ -161,7 +162,7 @@ class DmlPlanBuilder {
161162
}
162163

163164
PlanNode* build_delete(const sql_parser::AstNode* delete_ast) {
164-
if (!delete_ast) return nullptr;
165+
if (!supported(delete_ast)) return nullptr;
165166

166167
PlanNode* node = make_plan_node(arena_, PlanNodeType::DELETE_PLAN);
167168
if (!node) return nullptr;
@@ -201,6 +202,32 @@ class DmlPlanBuilder {
201202
}
202203

203204
private:
205+
static bool supported(const sql_parser::AstNode* ast) {
206+
if (!ast) return false;
207+
if constexpr (D == sql_parser::Dialect::PostgreSQL) {
208+
if (!PlanBuilder<D>::supports_dml_features(ast) || unsupported_dml(ast)) return false;
209+
}
210+
return true;
211+
}
212+
213+
static bool unsupported_dml(const sql_parser::AstNode* ast) {
214+
using sql_parser::NodeType;
215+
if (ast->type == NodeType::NODE_CTE || ast->type == NodeType::NODE_COMPOUND_QUERY ||
216+
ast->type == NodeType::NODE_ON_CONFLICT ||
217+
(ast->type == NodeType::NODE_IDENTIFIER && !(ast->flags & sql_parser::FLAG_IDENT_DELIMITED) &&
218+
ast->value().equals_ci("DEFAULT", 7))) return true;
219+
if (ast->type == NodeType::NODE_UPDATE_SET_ITEM && ast->first_child &&
220+
ast->first_child->type != NodeType::NODE_IDENTIFIER &&
221+
ast->first_child->type != NodeType::NODE_COLUMN_REF) return true;
222+
if (ast->type == NodeType::NODE_INSERT_COLUMNS) {
223+
for (const auto* col = ast->first_child; col; col = col->next_sibling)
224+
if (col->type != NodeType::NODE_IDENTIFIER) return true;
225+
}
226+
for (const auto* child = ast->first_child; child; child = child->next_sibling)
227+
if (unsupported_dml(child)) return true;
228+
return false;
229+
}
230+
204231
const Catalog& catalog_;
205232
sql_parser::Arena& arena_;
206233

‎include/sql_engine/plan_builder.h‎

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,64 @@ class PlanBuilder {
5656
return ast && !has_unsupported_query_feature(ast);
5757
}
5858

59+
// DML owns its VALUES rows; all expression/query restrictions still apply.
60+
static bool supports_dml_features(const sql_parser::AstNode* ast) {
61+
return ast && !has_unsupported_query_feature(ast, true, true);
62+
}
63+
5964
private:
6065
const Catalog& catalog_;
6166
sql_parser::Arena& arena_;
6267

63-
static bool has_unsupported_query_feature(const sql_parser::AstNode* node, bool root = true) {
68+
static bool has_unsupported_query_feature(const sql_parser::AstNode* node, bool root = true,
69+
bool dml_values = false) {
6470
using sql_parser::NodeType;
6571
switch (node->type) {
72+
// PG_GAPS_EXPRESSION_GUARD
73+
case NodeType::NODE_PG_EXTRACT:
74+
case NodeType::NODE_PG_SUBSTRING:
75+
case NodeType::NODE_PG_TIME_ZONE:
76+
case NodeType::NODE_PG_INTERVAL:
77+
case NodeType::NODE_PG_TRIM:
78+
case NodeType::NODE_PG_ARRAY_QUERY:
79+
case NodeType::NODE_PG_QUANTIFIED_OPERAND:
80+
case NodeType::NODE_PG_NORMALIZE:
81+
return true;
82+
// PG_GAPS_DML_GUARD
83+
case NodeType::NODE_MERGE_STMT:
84+
case NodeType::NODE_CTE_SEARCH:
85+
case NodeType::NODE_CTE_CYCLE:
86+
case NodeType::NODE_PG_DML_CLAUSE:
87+
case NodeType::NODE_PG_RETURNING_OPTIONS:
88+
return true;
89+
case NodeType::NODE_INSERT_STMT:
90+
case NodeType::NODE_UPDATE_STMT:
91+
case NodeType::NODE_DELETE_STMT:
92+
if (!root) return true; // A modifying CTE must never be materialized as a query.
93+
break;
94+
// PG_GAPS_DDL_GUARD
95+
case NodeType::NODE_PG_DDL_STMT:
96+
case NodeType::NODE_PG_DDL_CLAUSE:
97+
case NodeType::NODE_PG_DDL_LIST:
98+
case NodeType::NODE_PG_DDL_SYNTAX:
99+
return true;
100+
// PG_GAPS_QUERY_GUARD
101+
case NodeType::NODE_TABLE_REF:
102+
if (node->flags & (sql_parser::FLAG_TABLE_ONLY | sql_parser::FLAG_TABLE_INHERIT)) return true;
103+
break;
104+
case NodeType::NODE_GROUPING_SET:
105+
case NodeType::NODE_OFFSET_CLAUSE:
106+
case NodeType::NODE_FETCH_CLAUSE:
107+
case NodeType::NODE_ORDINALITY:
108+
case NodeType::NODE_FUNCTION_COLUMN:
109+
return true;
110+
case NodeType::NODE_GROUP_BY_CLAUSE:
111+
if (!node->value().empty()) return true;
112+
break;
113+
// PG_GAPS_JSON_XML_GUARD
114+
case NodeType::NODE_PG_JSON_XML:
115+
case NodeType::NODE_PG_JSON_XML_SYNTAX:
116+
return true;
66117
case NodeType::NODE_CTE:
67118
// Nested WITH requires its own materialization scope, which
68119
// neither build_cte nor the subquery executor implements.
@@ -74,8 +125,10 @@ class PlanBuilder {
74125
break;
75126
case NodeType::NODE_CTE_COLUMNS:
76127
case NodeType::NODE_TABLE_QUERY:
77-
case NodeType::NODE_VALUES_CLAUSE:
78128
return true;
129+
case NodeType::NODE_VALUES_CLAUSE:
130+
if (!dml_values) return true;
131+
break;
79132
case NodeType::NODE_BINARY_OP:
80133
case NodeType::NODE_UNARY_OP:
81134
if (node->flags & sql_parser::FLAG_PG_OPERATOR) return true;
@@ -101,7 +154,7 @@ class PlanBuilder {
101154
default: break;
102155
}
103156
for (const auto* child = node->first_child; child; child = child->next_sibling)
104-
if (has_unsupported_query_feature(child, false)) return true;
157+
if (has_unsupported_query_feature(child, false, dml_values)) return true;
105158
return false;
106159
}
107160

‎include/sql_parser/common.h‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ enum class StmtType : uint8_t {
119119
DO_STMT,
120120
COPY,
121121
RELEASE_SAVEPOINT,
122+
VACUUM,
123+
ANALYZE,
124+
MERGE,
122125
};
123126

124127
// -- AST node types --
@@ -276,6 +279,37 @@ enum class NodeType : uint16_t {
276279
NODE_NAMED_ARGUMENT, // value=argument name; one expression child
277280
NODE_AGGREGATE_ORDER_BY, // ORDER_BY_ITEM children; constants are not ordinals
278281
NODE_CTE_COLUMNS, // identifier children; follows body in CTE_DEFINITION
282+
// PG_GAPS_EXPRESSION_NODES
283+
NODE_PG_EXTRACT,
284+
NODE_PG_SUBSTRING,
285+
NODE_PG_TIME_ZONE,
286+
NODE_PG_INTERVAL,
287+
NODE_PG_TRIM,
288+
NODE_PG_ARRAY_QUERY,
289+
NODE_PG_QUANTIFIED_OPERAND,
290+
NODE_PG_NORMALIZE,
291+
// PG_GAPS_DML_NODES
292+
NODE_MERGE_STMT, // target, USING source, ON expression, WHEN actions, RETURNING
293+
NODE_MERGE_WHEN, // match spelling; optional AND expression, THEN action
294+
NODE_PG_DML_CLAUSE, // validated fixed syntax; structured children separated by spaces
295+
NODE_CTE_SEARCH, // value=DEPTH/BREADTH; columns then sequence column
296+
NODE_CTE_CYCLE, // columns, mark column, optional values, path column
297+
NODE_PG_RETURNING_OPTIONS, // OLD/NEW AS alias options
298+
NODE_PG_ASSIGNMENT_FIELD, // target.field, without expression parentheses
299+
// PG_GAPS_DDL_NODES
300+
NODE_PG_DDL_STMT, // command value; structured clause children
301+
NODE_PG_DDL_CLAUSE, // grammar production, optional keyword prefix
302+
NODE_PG_DDL_LIST, // comma-separated children; flag 1 = no parentheses
303+
NODE_PG_DDL_SYNTAX, // validated syntax keyword or option, never an expression
304+
// PG_GAPS_QUERY_NODES
305+
NODE_GROUPING_SET,
306+
NODE_OFFSET_CLAUSE,
307+
NODE_FETCH_CLAUSE,
308+
NODE_ORDINALITY,
309+
NODE_FUNCTION_COLUMN,
310+
// PG_GAPS_JSON_XML_NODES
311+
NODE_PG_JSON_XML,
312+
NODE_PG_JSON_XML_SYNTAX,
279313
};
280314

281315
} // namespace sql_parser

0 commit comments

Comments
 (0)