Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ public <S> Void visit(AllTableColumns allTableColumns, S context) {

@Override
public <S> Void visit(FunctionAllColumns functionAllColumns, S context) {

functionAllColumns.getFunction().accept(this, context);
return null;
}

Expand Down
58 changes: 58 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,55 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return getToken(i - 1).image.equals("*");
}

/**
* Detects PostgreSQL composite row expansion {@code (function()).*} (and any
* number of surrounding parentheses), where a parenthesised expression wrapping
* a single {@link Function} is immediately followed by {@code .*}.
*
* <p>This is a constant-time follower check (unwraps the already-parsed
* {@code retval} and peeks the next two tokens). It deliberately avoids the
* speculative syntactic lookahead that previously degraded performance, see
* issue #2207.
*
* @param retval the expression parsed so far within the
* {@code ParenthesedExpressionList} branch of {@code PrimaryExpression}
*/
protected boolean isFunctionAllColumnsAhead(Expression retval) {
// Fast follower gate: the overwhelming majority of parenthesised
// expressions are not followed by ".*", so reject on the token stream
// before ever touching the already-parsed expression.
if (!getToken(1).image.equals(".") || !getToken(2).image.equals("*")) {
return false;
}
if (retval == null) {
return false;
}

Expression inner = retval;
while (inner instanceof ParenthesedExpressionList) {
ParenthesedExpressionList<?> parenthesed = (ParenthesedExpressionList<?>) inner;
if (parenthesed.size() != 1) {
return false;
}
inner = parenthesed.get(0);
}

return inner instanceof Function;
}

/**
* Unwraps any number of surrounding parentheses from a parenthesised
* {@link Function} and returns the inner function. Only call this after
* {@link #isFunctionAllColumnsAhead(Expression)} has confirmed the shape.
*/
protected Function unwrapParenthesedFunction(Expression retval) {
Expression inner = retval;
while (inner instanceof ParenthesedExpressionList) {
inner = ((ParenthesedExpressionList<?>) inner).get(0);
}
return (Function) inner;
}

/**
* Follower-based disambiguation for reserved keywords in ambiguous
* positions (implicit alias, clause boundary, after parenthesised
Expand Down Expand Up @@ -7903,6 +7952,15 @@ Expression PrimaryExpression() #PrimaryExpression:
}
)

// PostgreSQL composite row expansion: (function()).*
// Re-enables FunctionAllColumns, which was disabled in #2207 due to the
// performance toll of a speculative syntactic lookahead. A bounded
// semantic follower check instead reads the already-parsed retval and
// peeks the next two tokens, avoiding any speculative production.
[ LOOKAHEAD( { isFunctionAllColumnsAhead(retval) } )
"." "*"
{ retval = new FunctionAllColumns(unwrapParenthesedFunction(retval)); } ]

// RowGet Expressions
( LOOKAHEAD(2) "." tmp=RelObjectName() { retval = new RowGetExpression(retval, tmp); } )*
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2019 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* PostgreSQL composite row expansion {@code (function_returning_composite).*}.
*
* <p>
* This feature was merged via #2207 and afterwards disabled, because the speculative syntactic
* lookahead used back then caused a severe performance regression. It is re-enabled here through a
* bounded semantic follower check, see {@code FunctionAllColumns} in the grammar.
*/
public class CompositeRowExpansionTest {

private static FunctionAllColumns assertFunctionAllColumns(String sql)
throws JSQLParserException {
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true);
Expression expression = select.getSelectItems().get(0).getExpression();
Assertions.assertTrue(expression instanceof FunctionAllColumns,
"Expected a FunctionAllColumns select item but got " + expression.getClass());
return (FunctionAllColumns) expression;
}

@Test
public void testIssue2412JsonPopulateRecord() throws JSQLParserException {
FunctionAllColumns result = assertFunctionAllColumns(
"SELECT (json_populate_record(NULL::users, data)).* FROM staging_users");
Assertions.assertEquals("json_populate_record", result.getFunction().getName());
}

@Test
public void testSimpleFunctionAllColumns() throws JSQLParserException {
FunctionAllColumns result = assertFunctionAllColumns("SELECT (foo(a, b)).* FROM t");
Assertions.assertEquals("foo", result.getFunction().getName());
}

@Test
public void testPgStatFileExampleFrom2207() throws JSQLParserException {
FunctionAllColumns result = assertFunctionAllColumns(
"SELECT (pg_stat_file('postgresql.conf')).*");
Assertions.assertEquals("pg_stat_file", result.getFunction().getName());
}

@Test
public void testIssue2412InsertSelectUseCase() throws JSQLParserException {
assertSqlCanBeParsedAndDeparsed(
"INSERT INTO users SELECT (json_populate_record(NULL::users, data)).* FROM staging_users",
true);
}

@Test
public void testMultipleSurroundingParensAreUnwrapped() throws JSQLParserException {
// Redundant parentheses around a single value are semantically transparent in
// PostgreSQL, so they are unwrapped to the inner function. The round-trip
// therefore normalises to a single surrounding pair.
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT ((((foo(a))))).* FROM t");
Expression expression = select.getSelectItems().get(0).getExpression();
Assertions.assertTrue(expression instanceof FunctionAllColumns);
Assertions.assertEquals("foo", ((FunctionAllColumns) expression).getFunction().getName());
Assertions.assertEquals("(foo(a)).*", expression.toString());
}

@Test
public void testParenthesedFunctionWithoutExpansionUnchanged() throws JSQLParserException {
// Without the trailing .* a parenthesised function stays a plain expression.
assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)) FROM t", true);
}

@Test
public void testRowGetExpressionAfterParenthesedFunctionUnchanged() throws JSQLParserException {
// (function()).name must keep parsing as a RowGetExpression, not be swallowed.
assertSqlCanBeParsedAndDeparsed("SELECT (foo(a, b)).colname FROM t", true);
}

@Test
public void testNonFunctionCompositeExpansionStillUnsupported() {
// Expanding an arbitrary (non-function) expression is out of scope and must
// keep failing cleanly instead of producing a wrong AST.
Assertions.assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse("SELECT (a + b).* FROM t"));
}
}
Loading