Skip to content

Commit f414da5

Browse files
committed
docs: describe PostgreSQL analysis syntax and AST ownership contracts
Document the new query grammar, supported transaction and COPY forms, and multi-statement parsing API with source offsets and per-statement results. State the lifetime requirements for input text and parser-arena ASTs, the limits of batch recovery, and the distinction between parsing a construct and executing it in the local query engine. Explain that window expressions now appear in emitted SQL and therefore change affected digest values. Add examples and contracts for bounded traversal, owning deep copies, validated subtree replacement and parameterization of copied ASTs. Cover PostgreSQL numbered binds, MySQL emitted parameter positions, literal/source mappings, preserved syntax constants, allocation behavior and contexts that must fail explicitly instead of yielding misleading rewritten SQL. Link both API guides from the README, correct its stale AstNode size claim to 48 bytes, and distinguish historical acceptance figures from complete-input AST coverage. Retain the implementation design and completion evidence with the user's current dedicated-branch instructions and benchmark exclusion. Documentation reflects the final forced build: 1,354 active C++ tests pass, 37 backend-dependent tests are skipped, 87 compatibility harness tests pass, and 33,534 refreshed CI fixtures replay successfully. Benchmark reports, harness code, build-ignore changes and new benchmark links remain uncommitted.
1 parent 363f641 commit f414da5

5 files changed

Lines changed: 323 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Dialect selection is compile-time (`Parser<Dialect::MySQL>` / `Parser<Dialect::P
5252

5353
See [`docs/benchmarks/latest.md`](docs/benchmarks/latest.md) for the full report, and [`REPRODUCING.md`](docs/benchmarks/REPRODUCING.md) for reproduction instructions.
5454

55+
The acceptance figures above are historical and include classification-only
56+
results; use the PostgreSQL compatibility report below for complete-input AST
57+
coverage and remaining syntax gaps.
58+
5559
## Quick Start
5660

5761
### Build
@@ -322,14 +326,19 @@ auto report = recovery.recover();
322326
323327
- **Arena allocator** — 64 KB bump allocator per parser, O(1) reset. All AST nodes and plan nodes live in the arena. No per-node new/delete.
324328
- **Zero-copy `StringRef`** — tokens point into the original input buffer.
325-
- **32-byte `AstNode`** — half a cache line; intrusive linked list (first_child + next_sibling).
329+
- **48-byte `AstNode`** — intrusive linked list (first_child + next_sibling), with value and source spans.
326330
- **Compile-time dialect dispatch** — `if constexpr` for MySQL vs PostgreSQL differences. Zero runtime overhead.
327331
- **Header-only parsers & operators** — maximum inlining. Only `arena.cpp`, `parser.cpp`, a few engine `.cpp` files compile separately.
328332
329333
## Features
330334
331335
### Parser
332336
337+
See [PostgreSQL analysis features](docs/postgresql-analysis-features.md) for
338+
DISTINCT ON, FILTER, LATERAL, named windows/frames, transaction/COPY ASTs and
339+
`parse_all()`. [AST utility APIs](docs/ast-utilities.md) document traversal,
340+
owning copies, subtree replacement and context-aware parameterization.
341+
333342
- **Tier 1 deep parse:** SELECT, INSERT, UPDATE, DELETE, SET, REPLACE, EXPLAIN, CALL, DO, LOAD DATA
334343
- **Compound queries:** UNION / INTERSECT / EXCEPT with SQL-standard precedence and parenthesized nesting
335344
- **CTEs:** `WITH ... [RECURSIVE] AS (...)` — non-recursive materialized, recursive planned

‎docs/ast-utilities.md‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# AST traversal, transformation, and parameterization
2+
3+
These header-only C++17 APIs use the existing 48-byte `AstNode` and `Arena`.
4+
Include `sql_parser/ast_walk.h`, `sql_parser/ast_transform.h`, or
5+
`sql_parser/parameterize.h` as needed.
6+
7+
## Traverse one subtree
8+
9+
```cpp
10+
auto result = sql_parser::walk_ast(root,
11+
[](const sql_parser::AstNode& node,
12+
const sql_parser::AstVisitContext& context) {
13+
// context.parent, context.depth, context.child_index
14+
return sql_parser::AstVisitAction::Continue;
15+
});
16+
```
17+
18+
Traversal is preorder, children in sibling order. `SkipChildren` skips only the
19+
current node's descendants; `Stop` terminates the whole walk. The root's own
20+
`next_sibling` is outside the traversal. A null root is a completed empty walk.
21+
Do not change links during a walk.
22+
23+
The iterative implementation uses O(depth) auxiliary storage. `AstWalkLimits`
24+
defaults to 1,000,000 visited nodes and a maximum depth of 4,096 (root depth zero).
25+
`AstWalkResult` contains the visit count and `Completed`, `Stopped`, or
26+
`LimitExceeded`. Bounds also prevent unbounded traversal of cyclic input.
27+
28+
## Copy and replace
29+
30+
```cpp
31+
sql_parser::Arena destination;
32+
auto copied = sql_parser::clone_ast(root, destination);
33+
if (!copied.ok()) { /* inspect copied.error */ }
34+
```
35+
36+
`clone_ast` copies one subtree and every value/source string into `destination`.
37+
The input SQL and original arena may then be destroyed. The clone's root has no
38+
sibling; internal child and sibling order and flags are retained. Cycles and
39+
shared nodes are rejected with `InvalidTree`; exhaustion and traversal bounds
40+
return explicit errors. Cloning null succeeds with a null result.
41+
42+
`make_owned_node(arena, type, value, flags, source)` is the owning counterpart of
43+
`make_node`: it copies text and returns null on arena allocation failure.
44+
45+
`replace_ast_subtree(root, target, replacement)` changes the caller's root pointer
46+
when necessary, preserving the target's sibling position. Pass null replacement
47+
to remove the target. On success the old target is detached from its siblings.
48+
The input root must be a standalone tree, with no root sibling. Replacement must
49+
be a detached subtree with no root sibling and share no nodes with the input
50+
tree. Validation happens before mutation; errors leave input links unchanged.
51+
Nodes are never freed individually, and callers retain responsibility for the
52+
lifetimes of every participating arena.
53+
54+
Failed operations can consume arena space; they do not reset or roll back an
55+
arena that might contain other live nodes. Temporary STL allocation failures use
56+
the usual C++ exception behavior.
57+
58+
## Parameterize complete statements
59+
60+
```cpp
61+
sql_parser::Parser<sql_parser::Dialect::PostgreSQL> parser;
62+
auto parsed = parser.parse(sql.data(), sql.size());
63+
sql_parser::Arena output;
64+
auto rewritten = sql_parser::parameterize_ast<sql_parser::Dialect::PostgreSQL>(
65+
parsed, output);
66+
if (rewritten.ok()) {
67+
sql_parser::Emitter<sql_parser::Dialect::PostgreSQL> emitter(output);
68+
emitter.emit(rewritten.ast);
69+
auto sql_with_binds = emitter.result();
70+
}
71+
```
72+
73+
Prefer the `ParseResult` overload: it rejects non-OK results, unconsumed input,
74+
and results without `full_input`. The raw `AstNode*` overload requires a complete,
75+
valid AST in the specified dialect. Neither overload performs catalog lookup,
76+
type inference, or a second SQL grammar validation.
77+
78+
The original tree is unchanged. The result contains a copied AST and an ordered
79+
`parameters` vector of `ExtractedParameter { index, literal_type, value, source }`.
80+
Indices are one-based. Values retain their lexical spelling, including any
81+
escape sequences; they are not decoded strings or converted numeric values.
82+
Source retains the original literal spelling when recorded by the parser, and
83+
may be empty for manually built nodes or keywords without source metadata.
84+
Mapping text and AST nodes live in the output arena; the result owns its vectors.
85+
Repeated literals produce separate parameters.
86+
87+
PostgreSQL preserves existing `$n` spellings and indices and generates new
88+
indices after the largest existing index. For `SELECT $3, 7, $1, 'a'`, the result
89+
is `SELECT $3, $4, $1, $5`, with new mapping indices 4 and 5 and
90+
`existing_parameters` equal to `[3, 1]`. Invalid indices and signed 32-bit index
91+
overflow fail explicitly.
92+
93+
MySQL emits `?` and counts positions in the resulting SQL. For
94+
`SELECT ?, 7, ?, 'a'`, new mapping indices are 2 and 4 and
95+
`existing_parameters` is `[1, 3]`. The latter identifies the positions where
96+
callers should interleave their original bindings. MySQL comma LIMIT syntax is
97+
normalized from `LIMIT offset, count` to `LIMIT count OFFSET offset`, so extracted
98+
literals follow the normalized emission order. If that comma LIMIT contains an
99+
existing `?`, parameterization returns `UnsupportedContext` rather than silently
100+
reordering the caller's original anonymous bindings. Existing binds elsewhere in
101+
the statement and `LIMIT count OFFSET offset` remain supported.
102+
103+
Supported roots are SELECT, INSERT, UPDATE, DELETE, compound queries, and TABLE
104+
queries. Bindable literals include integers, floats, strings, hex/bit literals,
105+
and standalone TRUE/FALSE. NULL remains literal. Direct integer ordinals in
106+
query GROUP BY, ORDER BY, and DISTINCT ON remain unchanged, including
107+
parenthesized integers. Integers inside larger expressions and window ORDER BY
108+
are values. `IS [NOT] TRUE/FALSE` keywords and PostgreSQL binary `::` type
109+
subtrees remain unchanged. The precision argument of CURRENT_TIMESTAMP,
110+
CURRENT_TIME, LOCALTIME, and LOCALTIMESTAMP remains a syntax constant. MySQL also
111+
preserves precision arguments of NOW, CURTIME, SYSDATE, UTC_TIME, and
112+
UTC_TIMESTAMP; these ordinary function names retain bindable arguments in
113+
PostgreSQL. Quoted function names retain bindable arguments in both dialects.
114+
Aggregate FILTER predicates and window frame offsets are bindable. A manually constructed binary `::` AST is supported; this does not
115+
extend the parser's existing cast grammar.
116+
117+
Unsupported roots and contexts return `UnsupportedRoot` or `UnsupportedContext`
118+
with no AST or partial mapping. These include SET/transaction/COPY/DDL/other
119+
utility statements, SELECT INTO, CTEs, generic function-shaped CAST nodes,
120+
opaque subquery/interval fragments, and unknown future node types. PostgreSQL
121+
string-shaped aliases are rejected, including typed string literals currently
122+
misparsed as aliases (for example, `SELECT TIMESTAMP '2020-01-01'`). These
123+
restrictions reflect contexts where replacing literals or the current emitter
124+
would not produce reliable executable SQL. Parameterization does not change the
125+
existing digest API.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# PostgreSQL query analysis additions
2+
3+
These additions extend parsing and SQL reconstruction. They do not add local
4+
execution support for the new PostgreSQL query constructs. The plan builder
5+
rejects unsupported constructs instead of silently executing a simpler query.
6+
7+
## Query syntax
8+
9+
- `SELECT DISTINCT ON (expressions)`.
10+
- Aggregate `FILTER (WHERE expression)`, also followed by `OVER`.
11+
- `LATERAL` subqueries and table functions, including qualified names and alias
12+
column lists.
13+
- Named window declarations (`WINDOW w AS (...)`), references (`OVER w`), and
14+
inherited specifications (`OVER (w ...)`).
15+
- `ROWS`, `RANGE`, and `GROUPS` frames; single bounds or `BETWEEN ... AND ...`;
16+
`UNBOUNDED`, offset `PRECEDING`/`FOLLOWING`, `CURRENT ROW`; and `EXCLUDE CURRENT
17+
ROW`, `GROUP`, `TIES`, or `NO OTHERS`.
18+
19+
These forms retain expressions and options as AST children for traversal and
20+
transformation. They support normal SQL emission and digest generation.
21+
PostgreSQL still performs name resolution, type checking, window inheritance
22+
validation and other semantic checks.
23+
24+
The new clauses remain bounded by the existing expression/subquery grammar.
25+
In particular, LATERAL currently uses the simple SELECT subquery callback;
26+
WITH/VALUES/compound derived-query operands and interval-valued frame offsets
27+
are not covered by this change.
28+
29+
Window expressions previously disappeared from emitted SQL and digests because
30+
their AST nodes had no emitter handlers. They now appear correctly. Digest
31+
strings/hashes for affected queries change as a consequence. Quoted identifiers
32+
and PostgreSQL string-source boundaries are also preserved more accurately.
33+
34+
## Multiple statements
35+
36+
```cpp
37+
sql_parser::Parser<sql_parser::Dialect::PostgreSQL> parser;
38+
std::string sql = "BEGIN READ ONLY; SELECT ';'; COMMIT";
39+
auto batch = parser.parse_all(sql.data(), sql.size());
40+
for (const auto& statement : batch.statements) {
41+
// statement.offset: byte offset in the original input
42+
// statement.source: SQL span, including its terminating semicolon if any
43+
// statement.result: classification, AST, status and full_input
44+
}
45+
```
46+
47+
All returned ASTs remain alive until the next `parse`, `parse_all`, or `reset`
48+
on that parser. Input SQL must remain alive too. Use `clone_ast` to retain a tree
49+
independently. The result vector owns records, not the ASTs or source bytes.
50+
51+
The tokenizer identifies boundaries, so semicolons in strings, quoted
52+
identifiers and comments do not split statements. Empty statements/comments are
53+
skipped. Malformed statements remain in the result; scanning continues at the
54+
next lexical statement boundary when available. Error offsets are relative to
55+
the original batch input.
56+
57+
`batch.ok()` requires every nonempty statement to have `OK`, complete input and
58+
a known statement type. It does not require every statement to have a deep AST
59+
and is not a substitute for PostgreSQL syntax/semantic validation. Empty batches
60+
succeed. The API handles SQL text, not inline COPY data, psql commands, or
61+
unquoted procedural bodies with internal statement terminators.
62+
63+
## Transaction and COPY ASTs
64+
65+
PostgreSQL transaction commands now produce `NODE_TRANSACTION_STMT`, whose value
66+
is the canonical command and whose children contain options, savepoint names or
67+
prepared-transaction identifiers:
68+
69+
- `BEGIN` / `START TRANSACTION`, isolation levels, read-only/read-write mode,
70+
and `[NOT] DEFERRABLE`.
71+
- `COMMIT` / `ROLLBACK`, `[AND [NO] CHAIN]`, and `END` / `ABORT` aliases.
72+
- `SAVEPOINT`, `ROLLBACK TO [SAVEPOINT]`, `RELEASE [SAVEPOINT]`.
73+
- `PREPARE TRANSACTION`, `COMMIT PREPARED`, `ROLLBACK PREPARED`.
74+
75+
`COPY` produces `NODE_COPY_STMT` with direction in its value (`FROM` or `TO`).
76+
Children are the table/query, optional column list, `NODE_COPY_ENDPOINT`, zero
77+
or more `NODE_COPY_OPTION` nodes, and an optional WHERE clause. Endpoints
78+
distinguish `STDIN`, `STDOUT`, `FILE`, and `PROGRAM`; filenames/commands are
79+
string children. Parsing does not execute any command or open any file.
80+
81+
Supported COPY forms include table/column lists, query `TO`, `FROM ... WHERE`,
82+
and modern parenthesized options: FORMAT, FREEZE, DELIMITER, NULL, DEFAULT,
83+
HEADER, QUOTE, ESCAPE, FORCE_QUOTE, FORCE_NOT_NULL, FORCE_NULL, ENCODING,
84+
ON_ERROR, REJECT_LIMIT and LOG_VERBOSITY. Query operands use the supported
85+
SELECT/TABLE/VALUES grammar. Legacy COPY option syntax and DML query operands
86+
are not implemented. Unsupported tails must not be treated as complete input.
87+
Option compatibility, privileges and data conversion remain server concerns.
88+
89+
The new `StmtType::COPY` and `StmtType::RELEASE_SAVEPOINT` values are appended;
90+
existing enum values and the 48-byte AST layout are unchanged. Consumers should
91+
handle the new statement types explicitly.
92+
93+
See [PostgreSQL COPY syntax](https://www.postgresql.org/docs/18/sql-copy.html)
94+
and [BEGIN syntax](https://www.postgresql.org/docs/18/sql-begin.html) for the server
95+
grammar and semantics.
96+
97+
## AST APIs and parameterization
98+
99+
See [AST utility APIs](ast-utilities.md) for traversal with skip/stop controls,
100+
owned cloning, validated subtree replacement, and parameterization with literal
101+
and existing-bind mappings. Parameterization operates on a copied AST, preserves
102+
query ordinals, and rejects unsupported contexts explicitly. It does not decode
103+
literal values or infer PostgreSQL types.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ParserSQL analysis features implementation plan
2+
3+
> Execute the authorized plan using test-driven development and independent
4+
> parallel tasks; integrate and review before reporting completion.
5+
6+
**Goal:** Improve PostgreSQL coverage and expose useful query-analysis APIs.
7+
**Architecture:** Extend the existing AST without changing its layout. Add
8+
header-only utilities and preserve the reusable parser's fast single-query path.
9+
**Tech stack:** C++17, arenas, GoogleTest, pinned PostgreSQL compatibility harness.
10+
**Spec:** ../specs/2026-09-22-parser-features-design.md
11+
12+
## Constraints and review focus
13+
14+
- Preserve existing uncommitted changes; append enum values; no new dependencies.
15+
- Commit implementation, tests and documentation on a dedicated branch with
16+
detailed messages; leave benchmark changes uncommitted. Raw build and timing
17+
artifacts stay in /tmp.
18+
- Check malformed new syntax, quoted identifiers, nested queries, parser reuse,
19+
allocation failure, and unsupported-engine behavior.
20+
- Parameter indices must remain correct when existing binds and literals mix.
21+
- Multi-statement lexical boundaries must ignore quoted/comment semicolons.
22+
23+
## Tasks
24+
25+
- [x] Query grammar: expression/select/table-reference parsers, common tags,
26+
emitter, planner guards, and focused grammar tests. Establish failing tests
27+
for DISTINCT ON, FILTER, LATERAL, named windows and frame boundaries; implement
28+
structured nodes; verify emission and invalid-input rejection.
29+
- [x] AST utilities: new ast_walk.h / ast_transform.h / parameterize.h and
30+
focused tests. Test traversal order, skip/stop, copied text lifetime, subtree
31+
replacement, ordinals, and bind mapping before implementation.
32+
- [x] Batch and utility parsing: parser.h / parser.cpp, utility parser header,
33+
tests. Test quoted semicolons, multiple preserved ASTs, malformed middle
34+
statements, transaction options and COPY direction/options; implement
35+
parse_all and utility dispatch, coordinate tags/emitter with grammar task.
36+
- [x] Integrate: register tests in Makefile, run forced full tests and corpus
37+
build, differential oracle validation and review; correct identified issues.
38+
- [x] Document public APIs, scope and limitations; rerun direct benchmark
39+
against the same pinned pg_raw_parse API and report actual coverage/timings.
40+
41+
## Completion evidence
42+
43+
1,354 active C++ tests passed (37 backend-dependent skips); corpus harness built;
44+
87 compatibility-harness tests and 33,534 regenerated CI cases passed; 26 AST
45+
utility tests passed ASan/UBSan. Independent review findings were fixed and
46+
reviewer signed off. Public APIs and limitations are documented in
47+
[the feature guide](../../postgresql-analysis-features.md) and
48+
[the AST utility guide](../../ast-utilities.md). Benchmark reports and harness
49+
changes are retained locally and excluded from the implementation commits.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ParserSQL PostgreSQL analysis features
2+
3+
The user authorized the recommended implementation sequence: common PostgreSQL
4+
query grammar, AST traversal/transformation, multi-statement and utility parsing,
5+
and context-aware parameterization. Existing uncommitted coverage and IN-list
6+
performance improvements are the starting point and must be preserved.
7+
8+
Use the existing C++17 parser and arenas. Keep AstNode at 48 bytes and append
9+
node tags without renumbering old ones. Existing parse(), digest and cache APIs
10+
keep their behavior. No dependency on PostgreSQL in the production library.
11+
12+
Grammar additions: DISTINCT ON, aggregate FILTER, LATERAL references, named
13+
windows and ROWS/RANGE/GROUPS frames with boundaries/exclusions. Each must have
14+
structured AST representation, emission, malformed-input checks and tests. The
15+
execution planner rejects newly parsed constructs it cannot execute.
16+
17+
AST utilities: preorder traversal with stop/skip, explicit arena-backed cloning
18+
that copies text, and controlled subtree replacement. Parameterization returns
19+
a copied AST plus an ordered parameter mapping; preserve original bind indices
20+
and SQL syntax constants/ordinals. Fail explicitly for unsupported roots or
21+
contexts instead of returning misleading executable SQL. Existing digest is
22+
unchanged.
23+
24+
Multi-statement API: return ordered per-statement parse results and source
25+
boundaries, retain all ASTs until parser reset, respect quoted strings/comments,
26+
report errors without accepting an unparsed tail. Utility parsing: structured
27+
PostgreSQL transaction options/savepoints and COPY relation/query, direction,
28+
endpoint and common options. COPY data payloads and execution are outside scope.
29+
30+
Validation: meaningful failing tests before implementation; focused tests per
31+
task; full forced C++ rebuild; corpus harness; PostgreSQL differential checks;
32+
direct pg_raw_parse benchmark for existing and new supported AST cases.
33+
Retain unsupported syntax explicitly. No claim of full PostgreSQL grammar or
34+
semantic validation. Commit relevant implementation, tests and documentation
35+
on a dedicated branch with detailed messages; keep benchmark changes uncommitted
36+
as requested by the user.

0 commit comments

Comments
 (0)