Skip to content

fix: support PostgreSQL composite row expansion (function()).* - #2463

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/composite-row-expansion-function-all-columns-2412
Aug 13, 2026
Merged

fix: support PostgreSQL composite row expansion (function()).*#2463
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/composite-row-expansion-function-all-columns-2412

Conversation

@fudianchn

Copy link
Copy Markdown
Contributor

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 original LOOKAHEAD(FunctionAllColumns()) at the PrimaryExpression entry was a speculative syntactic lookahead that triggered catastrophic backtracking on every primary expression (393 ms/op vs ~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-Function ParenthesedExpressionList followed by .*.

How

The feature is wired into the existing ParenthesedExpressionList branch of PrimaryExpression, but via a bounded semantic follower check instead of a speculative production:

  • A new helper isFunctionAllColumnsAhead(Expression) first peeks the next two tokens; only when they are . * does it unwrap nested ParenthesedExpressionList wrappers and confirm the inner expression is a Function. The common case (a parenthesised expression not followed by .*) therefore bails out in two comparisons, with no speculative production and no backtracking.
  • Only then is the result wrapped into the existing FunctionAllColumns.
  • TablesNamesFinder now 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 existing FunctionAllColumns AST 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

CompositeRowExpansionTest covers the issue case, simple/no-arg functions, the INSERT ... SELECT use case, multiple surrounding parentheses, and negative cases (no trailing .*, a RowGetExpression, a non-function expression). The positive tests fail on master and 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.parseSQLStatements on performance.sql, version=latest, 10 forks × 10 iterations (100 samples) on a dedicated 32-core host:

build ms/op
master (feature disabled) 3.632 ± 0.020
this change 3.639 ± 0.023

The +0.19% delta lies within the confidence intervals (the CIs overlap almost entirely) and is far from the 393 ms/op toll that motivated disabling the feature.

Verification of the original issue

SELECT (json_populate_record(NULL::users, data)).* FROM staging_users

fails on master (Encountered: ".") and parses + round-trips with this change, yielding a FunctionAllColumns select item.

Fixes #2412

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>
@manticore-projects

Copy link
Copy Markdown
Contributor

Good stuff, thank you for your contribution!

@manticore-projects
manticore-projects merged commit 7ced34d into JSQLParser:master Aug 13, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] JSQLParser 5.4-SNAPSHOT : PostgreSQL : json_populate_record row expansion with (…). * not supported

2 participants