diff --git a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java index 2f4b8614f..51e4057ec 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java @@ -9,9 +9,11 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class TableFunction extends Function implements FromItem { @@ -20,6 +22,7 @@ public class TableFunction extends Function implements FromItem { private Pivot pivot = null; private UnPivot unPivot = null; private Function function; + private ParenthesedExpressionList rowsFromFunctions; private String withClause = null; public TableFunction(Function function) { @@ -42,6 +45,27 @@ public TableFunction(String prefix, Function function, String withClause) { this.withClause = withClause; } + public TableFunction(ParenthesedExpressionList rowsFromFunctions) { + this.rowsFromFunctions = rowsFromFunctions; + } + + public TableFunction(String prefix, ParenthesedExpressionList rowsFromFunctions) { + this.prefix = prefix; + this.rowsFromFunctions = rowsFromFunctions; + } + + public TableFunction(ParenthesedExpressionList rowsFromFunctions, String withClause) { + this.rowsFromFunctions = rowsFromFunctions; + this.withClause = withClause; + } + + public TableFunction(String prefix, ParenthesedExpressionList rowsFromFunctions, + String withClause) { + this.prefix = prefix; + this.rowsFromFunctions = rowsFromFunctions; + this.withClause = withClause; + } + public TableFunction(String prefix, String name, Expression... parameters) { this.prefix = prefix; this.function = new Function(name, parameters); @@ -57,9 +81,32 @@ public Function getFunction() { public TableFunction setFunction(Function function) { this.function = function; + this.rowsFromFunctions = null; return this; } + public ParenthesedExpressionList getRowsFromFunctions() { + return rowsFromFunctions; + } + + public TableFunction setRowsFromFunctions( + ParenthesedExpressionList rowsFromFunctions) { + this.rowsFromFunctions = rowsFromFunctions; + this.function = null; + return this; + } + + public boolean isRowsFrom() { + return rowsFromFunctions != null; + } + + public List getFunctions() { + if (rowsFromFunctions != null) { + return rowsFromFunctions; + } + return function != null ? List.of(function) : null; + } + @Deprecated public Function getExpression() { return getFunction(); @@ -151,7 +198,11 @@ public StringBuilder appendTo(StringBuilder builder) { if (prefix != null) { builder.append(prefix).append(" "); } - builder.append(function.toString()); + if (rowsFromFunctions != null) { + builder.append("ROWS FROM ").append(rowsFromFunctions); + } else { + builder.append(function); + } if (withClause != null) { builder.append(" WITH ").append(withClause); diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 09ca9faba..a38cf3e81 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1302,7 +1302,11 @@ public Void visit(OracleHint hint, S context) { @Override public Void visit(TableFunction tableFunction, S context) { - visit(tableFunction.getFunction(), null); + if (tableFunction.getFunctions() != null) { + for (var function : tableFunction.getFunctions()) { + visit(function, null); + } + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index ba7d8ef3d..15aeb7c61 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -780,18 +780,7 @@ public StringBuilder visit(TableStatement tableStatement, S context) { @Override public StringBuilder visit(TableFunction tableFunction, S context) { - if (tableFunction.getPrefix() != null) { - builder.append(tableFunction.getPrefix()).append(" "); - } - tableFunction.getFunction().accept(this.expressionVisitor, context); - - if (tableFunction.getWithClause() != null) { - builder.append(" WITH ").append(tableFunction.getWithClause()); - } - - if (tableFunction.getAlias() != null) { - builder.append(tableFunction.getAlias()); - } + tableFunction.appendTo(builder); return builder; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8344b3309..aa6fe4fc7 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -5677,7 +5677,11 @@ FromItem FromItem() #FromItem: && getToken(3).kind == OPENING_BRACKET }) fromItem=TableFunction() | - LOOKAHEAD({ (isFunctionAhead() && getToken(1).kind != K_LATERAL) + LOOKAHEAD({ + (getToken(1).kind == K_ROWS + && getToken(2).kind == K_FROM + && getToken(3).kind == OPENING_BRACKET) + || (isFunctionAhead() && getToken(1).kind != K_LATERAL) || (getToken(1).kind == K_LATERAL && getToken(2).kind != OPENING_BRACKET) }) fromItem=TableFunction() | @@ -9704,12 +9708,15 @@ JsonTableFunction JsonTableBody() : { TableFunction TableFunction(): { Token prefix = null; - Function function; + Function function = null; + ParenthesedExpressionList rowsFromFunctions = null; Token withClause = null; } { [ prefix = ] ( + LOOKAHEAD(3) rowsFromFunctions = RowsFromFunctionList() + | LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.equalsIgnoreCase("JSON_TABLE") @@ -9721,16 +9728,44 @@ TableFunction TableFunction(): ) [ LOOKAHEAD(2) ( withClause = | withClause = ) ] { - return prefix!=null - ? withClause!=null + if (rowsFromFunctions != null) { + return prefix != null + ? withClause != null + ? new TableFunction(prefix.image, rowsFromFunctions, withClause.image) + : new TableFunction(prefix.image, rowsFromFunctions) + : withClause != null + ? new TableFunction(rowsFromFunctions, withClause.image) + : new TableFunction(rowsFromFunctions); + } + + return prefix != null + ? withClause != null ? new TableFunction(prefix.image, function, withClause.image) : new TableFunction(prefix.image, function) - : withClause!=null + : withClause != null ? new TableFunction(function, withClause.image) : new TableFunction(function); } } +ParenthesedExpressionList RowsFromFunctionList(): +{ + ParenthesedExpressionList functions = new ParenthesedExpressionList(); + Function function; +} +{ + "(" + function = Function() { functions.add(function); } + ( + "," + function = Function() { functions.add(function); } + )* + ")" + { + return functions; + } +} + List ColumnNamesWithParamsList() : { List colNames = new ArrayList(); String columnName; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/TableFunctionTest.java b/src/test/java/net/sf/jsqlparser/statement/select/TableFunctionTest.java index faf4cbe8c..3187e046c 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/TableFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/TableFunctionTest.java @@ -9,14 +9,14 @@ */ package net.sf.jsqlparser.statement.select; +import static org.junit.jupiter.api.Assertions.*; + import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class TableFunctionTest { @Test @@ -59,4 +59,22 @@ void testTableFunctionWithSupportedWithClauses(String withClause) throws JSQLPar String sqlStr = "SELECT * FROM UNNEST(ARRAY[1, 2, 3]) WITH " + withClause + " AS t(a, b)"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testRowsFromTableFunction() throws JSQLParserException { + String sqlStr = "SELECT *\n" + + "FROM ROWS FROM (\n" + + " generate_series(1,3),\n" + + " generate_series(10,12)\n" + + ") AS t(a,b)"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + TableFunction tableFunction = select.getFromItem(TableFunction.class); + + assertTrue(tableFunction.isRowsFrom()); + assertNotNull(tableFunction.getRowsFromFunctions()); + assertEquals(2, tableFunction.getRowsFromFunctions().size()); + assertEquals("generate_series", tableFunction.getRowsFromFunctions().get(0).getName()); + assertEquals("generate_series", tableFunction.getRowsFromFunctions().get(1).getName()); + } }