fix: support PostgreSQL composite row expansion (function()).* - #2463
Merged
manticore-projects merged 1 commit intoAug 13, 2026
Conversation
PostgreSQL expands a composite-returning function call into its columns with (function_call).*, e.g. SELECT (json_populate_record(NULL::users, data)).* FROM staging_users. JSQLParser rejected the trailing .* . Composite row expansion was implemented in JSQLParser#2207 but afterwards disabled, because the speculative syntactic LOOKAHEAD(FunctionAllColumns()) at the PrimaryExpression entry caused a severe regression (393 ms/op vs ~86 ms/op). The AST node, deparser, validator and all visitors stayed in place; only the grammar call site was commented out. Re-enable the feature without the speculative lookahead: after a ParenthesedExpressionList wrapping a single Function is parsed, a bounded semantic follower check (isFunctionAllColumnsAhead) peeks .* and wraps the result into FunctionAllColumns. The check first compares the next two tokens and only then unwraps the already-parsed expression, so the common path (a parenthesised expression not followed by .*) bails out in two comparisons without any speculative production or backtracking. TablesNamesFinder now descends into the wrapped function so column/table references inside the expansion are not lost. Scope: only (function_call).* is supported; arbitrary (non-function expression).* remains unsupported and fails cleanly as before. Redundant surrounding parentheses are unwrapped to the inner function. Performance (gradle jmh, parseSQLStatements on performance.sql, version=latest, 10 forks x 10 iterations, 100 samples, dedicated 32-core host): master (disabled): 3.632 +/- 0.020 ms/op this change: 3.639 +/- 0.023 ms/op The +0.19% delta lies within the confidence intervals and is far from the 393 ms/op toll that motivated disabling the feature. Testing: CompositeRowExpansionTest covers the issue case, simple and no-arg functions, the INSERT...SELECT use case, multiple surrounding parentheses, and negative cases (no trailing .*, RowGet expression, non-function expression). The positive tests fail on master and pass with this change. Fixes JSQLParser#2412 Signed-off-by: 付典 <fudianchn@gmail.com>
Contributor
|
Good stuff, thank you for your contribution! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Re-enable PostgreSQL composite row expansion
(function_call).*, e.g.SELECT (json_populate_record(NULL::users, data)).* FROM staging_users. JSQLParser currently rejects the trailing.*.This builds on #2207, which added the feature and then had to disable it.
Why
FunctionAllColumns(the AST node, deparser, validator and all four visitors) was fully implemented in #2207 but the grammar call site was commented out afterwards. The reason was a severe performance regression: the originalLOOKAHEAD(FunctionAllColumns())at thePrimaryExpressionentry was a speculative syntactic lookahead that triggered catastrophic backtracking on every primary expression (393 ms/opvs~86 ms/op). Issue #2412 is the user-visible symptom of that disabled feature.The maintainer explicitly invited a re-enablement "once performance can be proved" and suggested treating
(((( FUNCTION )))).*as a single-FunctionParenthesedExpressionListfollowed by.*.How
The feature is wired into the existing
ParenthesedExpressionListbranch ofPrimaryExpression, but via a bounded semantic follower check instead of a speculative production:isFunctionAllColumnsAhead(Expression)first peeks the next two tokens; only when they are.*does it unwrap nestedParenthesedExpressionListwrappers and confirm the inner expression is aFunction. The common case (a parenthesised expression not followed by.*) therefore bails out in two comparisons, with no speculative production and no backtracking.FunctionAllColumns.TablesNamesFindernow descends into the wrapped function, so column/table references inside the expansion are not lost.Root cause
Not a missing feature but a feature disabled for the wrong reason: the perf toll came from the speculative syntactic lookahead, not from the feature itself. Removing the speculation (post-parse follower check) keeps the feature without the toll.
Scope / limitation
Only
(function_call).*is supported (the shape the existingFunctionAllColumnsAST and #2207 cover). Arbitrary(non-function expression).*(e.g.(a + b).*,(tbl.col).*) remains unsupported and still fails cleanly, as before. Redundant surrounding parentheses are unwrapped to the inner function (so(((( f() )))).*round-trips as(f()).*, semantically equivalent).Testing
CompositeRowExpansionTestcovers the issue case, simple/no-arg functions, theINSERT ... SELECTuse case, multiple surrounding parentheses, and negative cases (no trailing.*, aRowGetExpression, a non-function expression). The positive tests fail onmasterand pass with this change.Local:
spotlessApply,checkstyleMain/Test,spotbugsMain/Test, and the full test suite (4731 tests) all green, no regression.Performance
gradle jmh,JSQLParserBenchmark.parseSQLStatementsonperformance.sql,version=latest, 10 forks × 10 iterations (100 samples) on a dedicated 32-core host:3.632 ± 0.0203.639 ± 0.023The
+0.19%delta lies within the confidence intervals (the CIs overlap almost entirely) and is far from the393 ms/optoll that motivated disabling the feature.Verification of the original issue
fails on
master(Encountered: ".") and parses + round-trips with this change, yielding aFunctionAllColumnsselect item.Fixes #2412