Skip to content

Commit 0deeafa

Browse files
authored
DuckDB: accept FROM-first queries and TABLE t as subqueries and set operands (#2799)
* DuckDB: accept FROM-first queries and TABLE t as subqueries and set operands - `(FROM t ...)` is a sub-select wherever `(SELECT ...)` is, and a parenthesized `FROM t` without pipe operators is a FROM-first select. - FROM-first queries and `TABLE t` are set operation operands, e.g. `FROM a UNION ALL FROM b` or `TABLE a EXCEPT TABLE b`, with a trailing ORDER BY/LIMIT applying to the whole operation. - `WITH ... TABLE t` keeps and prints its WITH clause. - The FROM-first scan follows the token chain and stops at the end of the enclosing parentheses. * Keep columns named table and print every clause of TABLE t - `(TABLE` starts a sub-select only when a table name follows and then the end of the query, so `(table = 'hits')` and `(table)` stay expressions on a column named table. `FROM (TABLE s.t) x` now parses. - A parenthesized `FROM t` without clauses stays a `FromQuery`. - `TableStatement` uses `Select.appendTo`, and its deparser prints OPTION, FETCH and the isolation level, so `TABLE t FETCH FIRST 1 ROWS ONLY` keeps its FETCH. - `CREATE TABLE ... AS` takes any query, including `TABLE a UNION TABLE b`.
1 parent a90873f commit 0deeafa

4 files changed

Lines changed: 128 additions & 31 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/select/TableStatement.java‎

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,6 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
3434
return builder;
3535
}
3636

37-
@SuppressWarnings({"PMD.CyclomaticComplexity"})
38-
@Override
39-
public StringBuilder appendTo(StringBuilder builder) {
40-
41-
appendSelectBodyTo(builder);
42-
43-
builder.append(orderByToString(false, orderByElements));
44-
45-
if (limit != null) {
46-
builder.append(limit);
47-
}
48-
if (offset != null) {
49-
builder.append(offset);
50-
}
51-
return builder;
52-
}
53-
5437
@Override
5538
public <T, S> T accept(SelectVisitor<T> selectVisitor, S context) {
5639
return selectVisitor.visit(this, context);

‎src/main/java/net/sf/jsqlparser/util/deparser/TableStatementDeParser.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import net.sf.jsqlparser.statement.piped.FromQuery;
1414
import net.sf.jsqlparser.statement.select.*;
1515

16+
import java.util.List;
17+
1618
/**
1719
* @author jxnu-liguobin
1820
*/
@@ -84,18 +86,39 @@ public <S> StringBuilder visit(LateralSubSelect lateralSubSelect, S context) {
8486

8587
@Override
8688
public <S> StringBuilder visit(TableStatement tableStatement, S context) {
89+
List<WithItem<?>> withItemsList = tableStatement.getWithItemsList();
90+
if (withItemsList != null && !withItemsList.isEmpty()) {
91+
SelectDeParser selectDeParser = new SelectDeParser(expressionVisitor, builder);
92+
builder.append("WITH ");
93+
for (int i = 0; i < withItemsList.size(); i++) {
94+
if (i > 0) {
95+
builder.append(", ");
96+
}
97+
withItemsList.get(i).accept((SelectVisitor<?>) selectDeParser, context);
98+
}
99+
builder.append(" ");
100+
}
87101
tableStatement.appendSelectBodyTo(builder);
88102
if (tableStatement.getOrderByElements() != null) {
89103
new OrderByDeParser(expressionVisitor, builder)
90104
.deParse(tableStatement.getOrderByElements());
91105
}
92106

107+
if (tableStatement.getOption() != null) {
108+
builder.append(tableStatement.getOption());
109+
}
93110
if (tableStatement.getLimit() != null) {
94111
new LimitDeparser(expressionVisitor, builder).deParse(tableStatement.getLimit());
95112
}
96113
if (tableStatement.getOffset() != null) {
97114
deparse(tableStatement.getOffset());
98115
}
116+
if (tableStatement.getFetch() != null) {
117+
builder.append(tableStatement.getFetch());
118+
}
119+
if (tableStatement.getIsolation() != null) {
120+
builder.append(tableStatement.getIsolation());
121+
}
99122

100123
// TODO UNION
101124

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,8 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
745745
// Direct subquery: ( SELECT/WITH/PIVOT ) → ParenthesedSelect handles it
746746
// ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect;
747747
// ParenthesedFromItem is checked first so Values-as-FROM-item works
748-
if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT) return false;
748+
if (k2 == K_SELECT || k2 == K_WITH || k2 == K_PIVOT || k2 == K_FROM) return false;
749+
if (k2 == K_TABLE && isTableQueryAhead(2)) return false;
749750
// ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it
750751
// Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem
751752
if (k2 == K_LATERAL) return false; // handled by LateralSubSelect
@@ -777,11 +778,35 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
777778
} catch (TokenMgrException e) { return false; }
778779
}
779780

781+
/**
782+
* Whether the TABLE at {@code offset} starts DuckDB's TABLE t query rather than naming a
783+
* column, as in (table = 'x'): a table name follows, and then the end of the query.
784+
*/
785+
private boolean isTableQueryAhead(int offset) {
786+
Token token = getToken(offset);
787+
do {
788+
token = nextWithItemToken(token);
789+
if (!isIdentifierishNameToken(token)) {
790+
return false;
791+
}
792+
token = nextWithItemToken(token);
793+
} while (".".equals(token.image));
794+
switch (token.kind) {
795+
case CLOSING_BRACKET: case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
796+
case K_ORDER: case K_LIMIT: case K_OFFSET: case K_FETCH:
797+
return true;
798+
default:
799+
return false;
800+
}
801+
}
802+
780803
protected boolean isParenthesedSelectAhead() {
781804
try {
782805
if (getToken(1).kind != OPENING_BRACKET) return false;
783806
int k = getToken(2).kind;
784-
return k == K_SELECT || k == K_WITH || k == K_VALUES;
807+
// DuckDB's (FROM t ...) and (TABLE t ...)
808+
return k == K_SELECT || k == K_WITH || k == K_VALUES || k == K_FROM
809+
|| k == K_TABLE && isTableQueryAhead(2);
785810
} catch (TokenMgrException e) {
786811
return false;
787812
}
@@ -1393,22 +1418,29 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
13931418
* nested in brackets (a sub-select, for instance) do not count.
13941419
*/
13951420
private boolean isFromFirstSelectAhead() {
1396-
if (getToken(1).kind != K_FROM) {
1421+
Token token = getToken(1);
1422+
if (token.kind != K_FROM) {
13971423
return false;
13981424
}
13991425
int depth = 0;
1400-
for (int i = 2;; i++) {
1401-
Token token = getToken(i);
1402-
if (token == null || token.kind == EOF || ";".equals(token.image)
1403-
|| "|>".equals(token.image)) {
1426+
while (true) {
1427+
token = nextWithItemToken(token);
1428+
if (token.kind == EOF || ";".equals(token.image) || "|>".equals(token.image)) {
14041429
return false;
14051430
}
14061431
if ("(".equals(token.image)) {
14071432
depth++;
14081433
} else if (")".equals(token.image)) {
1409-
depth--;
1434+
// the end of the parenthesized query this FROM starts
1435+
if (--depth < 0) {
1436+
return false;
1437+
}
14101438
} else if (depth == 0) {
14111439
switch (token.kind) {
1440+
case K_UNION:
1441+
case K_EXCEPT:
1442+
case K_INTERSECT:
1443+
case K_MINUS:
14121444
case K_SELECT:
14131445
case K_WHERE:
14141446
case K_GROUP:
@@ -3480,8 +3512,6 @@ Statement SingleStatement() :
34803512
|
34813513
stm = Select()
34823514
|
3483-
stm = TableStatement()
3484-
|
34853515
LOOKAHEAD(3) stm = Upsert()
34863516
|
34873517
LOOKAHEAD(2) stm = Alter()
@@ -7175,12 +7205,15 @@ Select Select() #Select:
71757205
{
71767206
[ with=WithList() ]
71777207
(
7178-
LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect()
7179-
|
7180-
LOOKAHEAD(3) select = FromQuery()
7208+
LOOKAHEAD(3, { !isFromFirstSelectAhead() }) select = FromQuery()
71817209
|
71827210
(
71837211
(
7212+
LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect()
7213+
|
7214+
// DuckDB's TABLE t, also as a set operation operand
7215+
LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement()
7216+
|
71847217
LOOKAHEAD(2) select = PivotQuery()
71857218
|
71867219
LOOKAHEAD(2) select = UnPivotQuery()
@@ -8146,6 +8179,10 @@ Select SetOperationList(Select select) #SetOperationList: {
81468179
)
81478180

81488181
(
8182+
LOOKAHEAD({ getToken(1).kind == K_FROM }) select = FromFirstSelect()
8183+
|
8184+
LOOKAHEAD({ getToken(1).kind == K_TABLE }) select = TableStatement()
8185+
|
81498186
select = PlainSelect()
81508187
|
81518188
select = Values()
@@ -8196,6 +8233,20 @@ Select SetOperationList(Select select) #SetOperationList: {
81968233
list.setIsolation(ps.getIsolation());
81978234
ps.setIsolation(null);
81988235
}
8236+
} else if (selects.get(selects.size() - 1) instanceof TableStatement) {
8237+
TableStatement table = (TableStatement) selects.get(selects.size() - 1);
8238+
if (table.getOrderByElements() != null) {
8239+
list.setOrderByElements(table.getOrderByElements());
8240+
table.setOrderByElements(null);
8241+
}
8242+
if (table.getLimit() != null) {
8243+
list.setLimit(table.getLimit());
8244+
table.setLimit(null);
8245+
}
8246+
if (table.getOffset() != null) {
8247+
list.setOffset(table.getOffset());
8248+
table.setOffset(null);
8249+
}
81998250
}
82008251
list.setBracketsOpsAndSelects(selects,operations);
82018252
return list;
@@ -15826,7 +15877,7 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1582615877
createTable.setExecute(execute);
1582715878
}
1582815879
|
15829-
( LOOKAHEAD(<K_TABLE>) select=TableStatement() | select=Select() )
15880+
select=Select()
1583015881
{ createTable.setSelect(select, false); }
1583115882
)
1583215883
[ LOOKAHEAD(2) <K_WITH> withData=DataPopulation()

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,4 +825,44 @@ void testLambdaAsIdentifier() throws JSQLParserException {
825825
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t DROP COLUMN lambda", true);
826826
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t RENAME lambda TO x", true);
827827
}
828+
829+
@Test
830+
void testFromFirstAndTableAsQueryOperands() throws JSQLParserException {
831+
for (String sqlStr : new String[] {"SELECT * FROM (FROM t)",
832+
"SELECT * FROM (FROM t WHERE a > 1) x JOIN (SELECT 1) y ON true",
833+
"SELECT * FROM t WHERE a IN (FROM u)", "SELECT (FROM u SELECT max(a))",
834+
"FROM a UNION ALL FROM b", "FROM a WHERE x > 1 UNION ALL SELECT 1",
835+
"SELECT 1 EXCEPT FROM b WHERE x", "FROM a SELECT x UNION FROM b SELECT x",
836+
"WITH t AS (SELECT 1), u AS (SELECT 2) TABLE t", "TABLE a UNION TABLE b",
837+
"SELECT 1 UNION TABLE b", "(TABLE a EXCEPT ALL TABLE b)",
838+
"SELECT * FROM TABLE(f(1))"}) {
839+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
840+
}
841+
842+
SetOperationList union = (SetOperationList) CCJSqlParserUtil
843+
.parse("TABLE a UNION TABLE b ORDER BY 1 LIMIT 2");
844+
Assertions.assertEquals("1", union.getOrderByElements().get(0).toString());
845+
Assertions.assertNotNull(union.getLimit());
846+
Assertions.assertNull(((TableStatement) union.getSelect(1)).getOrderByElements());
847+
848+
union = (SetOperationList) CCJSqlParserUtil.parse("FROM a UNION FROM b ORDER BY 1");
849+
Assertions.assertTrue(((PlainSelect) union.getSelect(0)).isFromFirst());
850+
Assertions.assertEquals("1", union.getOrderByElements().get(0).toString());
851+
}
852+
853+
@Test
854+
void testTableQueryKeepsTableColumnsAndTrailingClauses() throws JSQLParserException {
855+
// TABLE also names columns, e.g. in ClickHouse's system.parts
856+
for (String sqlStr : new String[] {
857+
"SELECT database, table FROM system.parts WHERE (database = 'default') AND (table = 'hits')",
858+
"SELECT (table), (table.a), (table + 1) FROM t",
859+
"SELECT a FROM t WHERE (table, b) IN ((1, 2))",
860+
"SELECT * FROM (TABLE s.t) x", "TABLE t FETCH FIRST 1 ROWS ONLY",
861+
"WITH a AS (SELECT 1) TABLE a FETCH FIRST 1 ROWS ONLY",
862+
"CREATE TABLE x AS TABLE a UNION TABLE b"}) {
863+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
864+
}
865+
Assertions.assertInstanceOf(net.sf.jsqlparser.statement.piped.FromQuery.class,
866+
((ParenthesedSelect) CCJSqlParserUtil.parse("(FROM t)")).getSelect());
867+
}
828868
}

0 commit comments

Comments
 (0)