GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — fix: give every parsed object a node spanning its source by knutwannheden · Pull Request #2870 · JSQLParser/JSqlParser Conversation
|
| over the test suite's SQL | master | this PR |
|---|---|---|
ASTNodeAccess objects without a node |
6,575 | 0 |
| ranges outside the object that holds them | 126 | 0 |
| expression ranges whose text parses to something else | 50 | 0 |
On master, statements, OrderByElement and ColDataType do not implement ASTNodeAccess, so they are not counted.
RangeNode. Objects that no grammar rule gives a node are linked to a RangeNode, a node outside the parse tree that records the object's first and last token. These are objects built in the Pratt operator loops, in productions without a # node, or owned by the model, such as a PARTITION BY clause. getASTRoot() returns the same tree, and no tree node changes its value or range.WITH, an Oracle PRIOR, a column's (+), LIMIT ... BY, sequence options, IF ... ELSE and merged adjacent literals. The object moves to a RangeNode over its full text, which answers its tree node's parent, children and id, so getParent() and tree walks from the object's node are unchanged. A string literal no longer takes the node of the primary expression around it, as in 'a'::text.SimpleCharStream now records where each match began. CommonTokenAction then gives the comments before each token their positions, and an Oracle hint gets a range.OrderByElement, ColDataType, and the 118 statement classes that implemented only Statement now extend ASTNodeAccessImpl. SingleStatement(), blocks and unsupported statements link them. The statement sweep is its own commit.ASTNodeCoverageTest parses the SQL files under src/test/resources with error recovery, the Oracle tests in the Oracle dialect, and walks every object through its getters. Each ASTNodeAccess object must have a node that:
getParent() returns;The same checks run over statements the corpus lacks, such as data-modifying CTEs and Oracle blocks. Another test pins ranges the checks cannot judge, such as WHEN, data types, PARTITION BY, (+), LIMIT BY and hints. Each check fails when one of the fixes is undone.
getParent() returns null when it runs out of tree, instead of throwing. A RangeNode for an object with no tree node has no parent.prattArithRest and prattExpressionRest take the token their operand chain starts at.INSERT ... VALUES allocates 3.2% more and retains the same, and its parse time shows no difference: 3 pairs faster, 2 slower, 1 tied.app@localhost, has no token of its own.ASTNodeAccess, such as Alias, ColumnDefinition, WithItem, GroupByElement and UpdateSet, still have no range.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
prattArithRest builds operator expressions in plain Java, outside any production, so JJTree never opened a node for them and `CURRENT_DATE - 7` or `a || b` had no source range. Each expression the loop builds is now linked to a node from the token its operand chain starts at to the last token consumed. The node stays out of the tree, so the operands' nodes keep their place.
|
Converting this to a draft while I extend it to the other expressions that still come out of the parser without an AST node. Going through the test corpus turned up nested |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
The statement classes that implemented Statement alone now extend ASTNodeAccessImpl, as Select and Import already did, so a parsed statement can hold the node that records its source range. Nothing links them yet.
Objects no grammar rule opens a node for, such as a column's table qualifier, a WHEN clause, nested AND/OR, IS NULL, BETWEEN, parameter lists, data types, ORDER BY elements and statements, are linked to a RangeNode: a node outside the parse tree that records the object's first and last token. getASTRoot() returns the same tree, and no tree node gains or changes its value. Ranges that missed text their object prints now cover it: a statement's WITH, Oracle PRIOR and (+), LIMIT ... BY, sequence options, IF ... ELSE and merged adjacent literals. A string literal no longer takes the node of the primary expression around it. Comments get positions, so an Oracle hint has a range too. getParent() returns null instead of throwing once it runs out of tree. ASTNodeCoverageTest parses the test resources' SQL and requires every ASTNodeAccess object to have a node of at least one character, inside its holder's range, never partly overlapping another, holding no parser, and, for expressions and statements, covering text that parses back to the same object.
Statements that never pass through SingleStatement get their node where they are built: data-modifying CTEs, statements inside an Oracle block, CREATE SCHEMA elements, CREATE TABLE ... AS EXECUTE and the GRANT or REVOKE of ALTER DEFAULT PRIVILEGES. An IF statement's range takes in the semicolon it prints. A RangeNode that widens a tree node answers its children, id and dump too, so a WITH statement's node still shows the tree beneath it. Comments are positioned in one backwards pass over the recorded match begins, and comments before the end of input are positioned once parsing ends, since the end-of-file token gets no token action. Values in a primary expression, string literals, expression lists and row patterns get a RangeNode only once something wraps them, so a production's own tree node is the only node most of them get. On a 20,000-row INSERT ... VALUES this cuts the added allocation from 12.9% to 3.2%. The coverage test parses the Oracle tests in the Oracle dialect, checks that getParent() encloses each range, and runs its checks over the statements above, which the test corpus does not contain.
There was an error while loading. Please reload this page.
|
Please fix Codacy issues. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
`visit` and `checkRange` exceeded PMD's NPath threshold of 200, so the container walk and the parent check move into methods of their own. `isHeld` compares with `==` on purpose: a getter that answers a new object on every call computes its child rather than holding it.
|
Thanks! The Codacy findings are fixed in e8de6e9. All four were in the new I ran
This branch parses |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Good Job, give me a few minutes for proof reading everything. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
`Condition()` created a deque for the tokens of a leading `NOT` on every call, though few conditions have one. Each empty `ArrayDeque` allocates its 16-slot array up front, so parsing `performance.sql` allocated 585 KB more per parse. The deque is now created at the first `NOT`.
|
Most of that came from my own change: a deque
What remains is the range nodes themselves.
The branch now averages 0.7% slower, less than master moved between its own two runs (2.3%). Deriving ranges from operands would remove only a small part of what is left, so I'd leave that for a follow-up. The same profile shows a few allocations on master worth trimming, such as the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Only the generated parser creates a `RangeNode`, and callers see it as the `Node` that `getASTNode()` returns. Keeping the class package-private leaves its design free to change before a release.
|
Looking past this PR: for the next major release, I think it would be cleaner to stop exposing the parser's internals through the AST.
What callers need is the source range. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Nevermind, it is clear. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I tried to resolve the merge conflicts, but probably made it worse. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Conflicts in JSqlParserCC.jjt, resolved by keeping master's logic and re-adding the node links on top: - PARTITION BY (JSQLParser#2878): the four sites call ComplexExpressionList() directly and still link the PARTITION BY node. - PrimaryExpression (JSQLParser#2874): innerSigns is null without an inner sign, so the loop that wraps and links signed values is guarded. - Condition (JSQLParser#2874): one lazily created deque of NOT tokens carries both the token for the node and whether it was spelled "!".
f2b91bf to
9f12b5f
Compare
There was an error while loading. Please reload this page.
|
Thank you much! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
JMH before/after for this PR as merged: dd03d6f against its parent df97e40. Three interleaved rounds of Parse time stays within that noise, while allocation goes up by about 4%: Parse time is +1.0% on average across the rounds. The control moves between -1.3% and +0.2% across the same runs, so any slowdown is within what this setup can resolve. Allocation rises a steady 4.2–4.5%, about 260 KB per parse of |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Yes, I had confirmed that before. Awesome work and much appreciated! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.