diff --git a/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java b/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java index 58072fa55..22d2d0cb4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java @@ -34,23 +34,6 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity"}) - @Override - public StringBuilder appendTo(StringBuilder builder) { - - appendSelectBodyTo(builder); - - builder.append(orderByToString(false, orderByElements)); - - if (limit != null) { - builder.append(limit); - } - if (offset != null) { - builder.append(offset); - } - return builder; - } - @Override public T accept(SelectVisitor selectVisitor, S context) { return selectVisitor.visit(this, context); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java index 663cd1f04..8789ab7e2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java @@ -13,6 +13,8 @@ import net.sf.jsqlparser.statement.piped.FromQuery; import net.sf.jsqlparser.statement.select.*; +import java.util.List; + /** * @author jxnu-liguobin */ @@ -84,18 +86,39 @@ public StringBuilder visit(LateralSubSelect lateralSubSelect, S context) { @Override public StringBuilder visit(TableStatement tableStatement, S context) { + List> withItemsList = tableStatement.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + SelectDeParser selectDeParser = new SelectDeParser(expressionVisitor, builder); + builder.append("WITH "); + for (int i = 0; i < withItemsList.size(); i++) { + if (i > 0) { + builder.append(", "); + } + withItemsList.get(i).accept((SelectVisitor) selectDeParser, context); + } + builder.append(" "); + } tableStatement.appendSelectBodyTo(builder); if (tableStatement.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder) .deParse(tableStatement.getOrderByElements()); } + if (tableStatement.getOption() != null) { + builder.append(tableStatement.getOption()); + } if (tableStatement.getLimit() != null) { new LimitDeparser(expressionVisitor, builder).deParse(tableStatement.getLimit()); } if (tableStatement.getOffset() != null) { deparse(tableStatement.getOffset()); } + if (tableStatement.getFetch() != null) { + builder.append(tableStatement.getFetch()); + } + if (tableStatement.getIsolation() != null) { + builder.append(tableStatement.getIsolation()); + } // TODO UNION diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 64172751a..2bd483cec 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -745,7 +745,8 @@ public class CCJSqlParser extends AbstractJSqlParser { // Direct subquery: ( SELECT/WITH/PIVOT ) → ParenthesedSelect handles it // ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect; // ParenthesedFromItem is checked first so Values-as-FROM-item works - if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT) return false; + if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT || k2 == K_FROM) return false; + if (k2 == K_TABLE && isTableQueryAhead(2)) return false; // ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it // Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem if (k2 == K_LATERAL) return false; // handled by LateralSubSelect @@ -777,11 +778,35 @@ public class CCJSqlParser extends AbstractJSqlParser { } catch (TokenMgrException e) { return false; } } + /** + * Whether the TABLE at {@code offset} starts DuckDB's TABLE t query rather than naming a + * column, as in (table = 'x'): a table name follows, and then the end of the query. + */ + private boolean isTableQueryAhead(int offset) { + Token token = getToken(offset); + do { + token = nextWithItemToken(token); + if (!isIdentifierishNameToken(token)) { + return false; + } + token = nextWithItemToken(token); + } while (".".equals(token.image)); + switch (token.kind) { + case CLOSING_BRACKET: case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + case K_ORDER: case K_LIMIT: case K_OFFSET: case K_FETCH: + return true; + default: + return false; + } + } + protected boolean isParenthesedSelectAhead() { try { if (getToken(1).kind != OPENING_BRACKET) return false; int k = getToken(2).kind; - return k == K_SELECT || k == K_WITH || k == K_VALUES; + // DuckDB's (FROM t ...) and (TABLE t ...) + return k == K_SELECT || k == K_WITH || k == K_VALUES || k == K_FROM + || k == K_TABLE && isTableQueryAhead(2); } catch (TokenMgrException e) { return false; } @@ -1393,22 +1418,29 @@ public class CCJSqlParser extends AbstractJSqlParser { * nested in brackets (a sub-select, for instance) do not count. */ private boolean isFromFirstSelectAhead() { - if (getToken(1).kind != K_FROM) { + Token token = getToken(1); + if (token.kind != K_FROM) { return false; } int depth = 0; - for (int i = 2;; i++) { - Token token = getToken(i); - if (token == null || token.kind == EOF || ";".equals(token.image) - || "|>".equals(token.image)) { + while (true) { + token = nextWithItemToken(token); + if (token.kind == EOF || ";".equals(token.image) || "|>".equals(token.image)) { return false; } if ("(".equals(token.image)) { depth++; } else if (")".equals(token.image)) { - depth--; + // the end of the parenthesized query this FROM starts + if (--depth < 0) { + return false; + } } else if (depth == 0) { switch (token.kind) { + case K_UNION: + case K_EXCEPT: + case K_INTERSECT: + case K_MINUS: case K_SELECT: case K_WHERE: case K_GROUP: @@ -3475,8 +3507,6 @@ Statement SingleStatement() : | stm = Select() | - stm = TableStatement() - | LOOKAHEAD(3) stm = Upsert() | LOOKAHEAD(2) stm = Alter() @@ -7170,12 +7200,15 @@ Select Select() #Select: { [ with=WithList() ] ( - LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect() - | - LOOKAHEAD(3) select = FromQuery() + LOOKAHEAD(3, { !isFromFirstSelectAhead() }) select = FromQuery() | ( ( + LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect() + | + // DuckDB's TABLE t, also as a set operation operand + LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement() + | LOOKAHEAD(2) select = PivotQuery() | LOOKAHEAD(2) select = UnPivotQuery() @@ -8141,6 +8174,10 @@ Select SetOperationList(Select select) #SetOperationList: { ) ( + LOOKAHEAD({ getToken(1).kind == K_FROM }) select = FromFirstSelect() + | + LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement() + | select = PlainSelect() | select = Values() @@ -8191,6 +8228,20 @@ Select SetOperationList(Select select) #SetOperationList: { list.setIsolation(ps.getIsolation()); ps.setIsolation(null); } + } else if (selects.get(selects.size() - 1) instanceof TableStatement) { + TableStatement table = (TableStatement) selects.get(selects.size() - 1); + if (table.getOrderByElements() != null) { + list.setOrderByElements(table.getOrderByElements()); + table.setOrderByElements(null); + } + if (table.getLimit() != null) { + list.setLimit(table.getLimit()); + table.setLimit(null); + } + if (table.getOffset() != null) { + list.setOffset(table.getOffset()); + table.setOffset(null); + } } list.setBracketsOpsAndSelects(selects,operations); return list; @@ -15821,7 +15872,7 @@ CreateTable CreateTable(boolean isUsingOrReplace): createTable.setExecute(execute); } | - ( LOOKAHEAD() select=TableStatement() | select=Select() ) + select=Select() { createTable.setSelect(select, false); } ) [ LOOKAHEAD(2) withData=DataPopulation() diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java index fbc09b52c..09f79b86e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java @@ -825,4 +825,44 @@ void testLambdaAsIdentifier() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t DROP COLUMN lambda", true); TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t RENAME lambda TO x", true); } + + @Test + void testFromFirstAndTableAsQueryOperands() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT * FROM (FROM t)", + "SELECT * FROM (FROM t WHERE a > 1) x JOIN (SELECT 1) y ON true", + "SELECT * FROM t WHERE a IN (FROM u)", "SELECT (FROM u SELECT max(a))", + "FROM a UNION ALL FROM b", "FROM a WHERE x > 1 UNION ALL SELECT 1", + "SELECT 1 EXCEPT FROM b WHERE x", "FROM a SELECT x UNION FROM b SELECT x", + "WITH t AS (SELECT 1), u AS (SELECT 2) TABLE t", "TABLE a UNION TABLE b", + "SELECT 1 UNION TABLE b", "(TABLE a EXCEPT ALL TABLE b)", + "SELECT * FROM TABLE(f(1))"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + SetOperationList union = (SetOperationList) CCJSqlParserUtil + .parse("TABLE a UNION TABLE b ORDER BY 1 LIMIT 2"); + Assertions.assertEquals("1", union.getOrderByElements().get(0).toString()); + Assertions.assertNotNull(union.getLimit()); + Assertions.assertNull(((TableStatement) union.getSelect(1)).getOrderByElements()); + + union = (SetOperationList) CCJSqlParserUtil.parse("FROM a UNION FROM b ORDER BY 1"); + Assertions.assertTrue(((PlainSelect) union.getSelect(0)).isFromFirst()); + Assertions.assertEquals("1", union.getOrderByElements().get(0).toString()); + } + + @Test + void testTableQueryKeepsTableColumnsAndTrailingClauses() throws JSQLParserException { + // TABLE also names columns, e.g. in ClickHouse's system.parts + for (String sqlStr : new String[] { + "SELECT database, table FROM system.parts WHERE (database = 'default') AND (table = 'hits')", + "SELECT (table), (table.a), (table + 1) FROM t", + "SELECT a FROM t WHERE (table, b) IN ((1, 2))", + "SELECT * FROM (TABLE s.t) x", "TABLE t FETCH FIRST 1 ROWS ONLY", + "WITH a AS (SELECT 1) TABLE a FETCH FIRST 1 ROWS ONLY", + "CREATE TABLE x AS TABLE a UNION TABLE b"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + Assertions.assertInstanceOf(net.sf.jsqlparser.statement.piped.FromQuery.class, + ((ParenthesedSelect) CCJSqlParserUtil.parse("(FROM t)")).getSelect()); + } }