Skip to content

Commit 49b5ea9

Browse files
authored
DuckDB: support LIMIT 10% and TABLESAMPLE without a method (#2802)
* DuckDB: support LIMIT 10% and TABLESAMPLE without a method - `LIMIT 10%` and `LIMIT 10 PERCENT` set the new `Limit.percentUnit`. Inside a LIMIT, a `%` followed by the end of the clause is the unit, not a modulo; `LIMIT 10 % 3` and `LIMIT a % offset` are unchanged. - TABLESAMPLE takes DuckDB's optional method and reservoir, as USING SAMPLE already does: `TABLESAMPLE 10%`, `TABLESAMPLE reservoir(10 ROWS)`. - A table function keeps and prints its sample clause: `range(10) r USING SAMPLE 10%` used to lose it. * Find the percent of LIMIT 10% once and keep columns named offset - LIMIT scans its clause once for the top-level % followed by its end, and the Pratt loop stops at that token only, so `LIMIT a % offset + 1` and modulos nested in sub-selects are unchanged. - OFFSET ends the clause only before a value, FETCH only before FIRST or NEXT. - After DuckDB's TABLESAMPLE n, OFFSET belongs to the query. - The unit is printed after NULL and ALL too.
1 parent ac8a4f0 commit 49b5ea9

5 files changed

Lines changed: 160 additions & 27 deletions

File tree

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class Limit extends ASTNodeAccessImpl {
3232
*/
3333
private ExpressionList<Expression> byExpressions;
3434

35+
private String percentUnit;
36+
3537
public Expression getOffset() {
3638
return offset;
3739
}
@@ -72,19 +74,39 @@ public void setLimitNull(boolean b) {
7274
}
7375
}
7476

77+
/**
78+
* DuckDB's {@code %} or {@code PERCENT} after the row count, which then is a percentage of the
79+
* rows, or {@code null}.
80+
*/
81+
public String getPercentUnit() {
82+
return percentUnit;
83+
}
84+
85+
public Limit setPercentUnit(String percentUnit) {
86+
this.percentUnit = percentUnit;
87+
return this;
88+
}
89+
90+
String percentUnitToString() {
91+
if (percentUnit == null) {
92+
return "";
93+
}
94+
return "%".equals(percentUnit) ? "%" : " " + percentUnit;
95+
}
96+
7597
@Override
7698
public String toString() {
7799
String retVal = " LIMIT ";
78100

79101
if (rowCount instanceof AllValue || rowCount instanceof NullValue) {
80102
// no offset allowed
81-
retVal += rowCount;
103+
retVal += rowCount + percentUnitToString();
82104
} else {
83105
if (null != offset) {
84106
retVal += offset + ", ";
85107
}
86108
if (null != rowCount) {
87-
retVal += rowCount;
109+
retVal += rowCount + percentUnitToString();
88110
}
89111
}
90112

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
@SuppressWarnings({"PMD.UncommentedEmptyMethodBody"})
2020
public class TableFunction extends Function implements FromItem {
21+
private SampleClause sampleClause;
22+
2123
private String prefix = null;
2224
private Alias alias = null;
2325
private Pivot pivot = null;
@@ -220,12 +222,13 @@ public TableFunction withUnPivot(UnPivot unpivot) {
220222

221223
@Override
222224
public SampleClause getSampleClause() {
223-
return null;
225+
return sampleClause;
224226
}
225227

226228
@Override
227-
public FromItem setSampleClause(SampleClause sampleClause) {
228-
return null;
229+
public TableFunction setSampleClause(SampleClause sampleClause) {
230+
this.sampleClause = sampleClause;
231+
return this;
229232
}
230233

231234
public StringBuilder appendTo(StringBuilder builder) {
@@ -263,6 +266,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expres
263266
builder.append(offsetAlias);
264267
}
265268
}
269+
if (sampleClause != null) {
270+
sampleClause.appendTo(builder);
271+
}
266272
return builder;
267273
}
268274

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,18 @@ public void deParse(Limit limit) {
2727

2828
public <S> void deParse(Limit limit, S context) {
2929
builder.append(" LIMIT ");
30-
if (limit.isLimitNull()) {
31-
builder.append("NULL");
30+
if (limit.isLimitNull() || limit.isLimitAll()) {
31+
builder.append(limit.isLimitNull() ? "NULL" : "ALL");
32+
appendPercentUnit(limit);
3233
} else {
33-
if (limit.isLimitAll()) {
34-
builder.append("ALL");
35-
} else {
36-
if (null != limit.getOffset()) {
37-
limit.getOffset().accept(expressionVisitor, context);
38-
builder.append(", ");
39-
}
40-
41-
if (null != limit.getRowCount()) {
42-
limit.getRowCount().accept(expressionVisitor, context);
43-
}
34+
if (null != limit.getOffset()) {
35+
limit.getOffset().accept(expressionVisitor, context);
36+
builder.append(", ");
37+
}
38+
39+
if (null != limit.getRowCount()) {
40+
limit.getRowCount().accept(expressionVisitor, context);
41+
appendPercentUnit(limit);
4442
}
4543
}
4644

@@ -50,6 +48,12 @@ public <S> void deParse(Limit limit, S context) {
5048
}
5149
}
5250

51+
private void appendPercentUnit(Limit limit) {
52+
if (limit.getPercentUnit() != null) {
53+
builder.append("%".equals(limit.getPercentUnit()) ? "%" : " " + limit.getPercentUnit());
54+
}
55+
}
56+
5357
public ExpressionVisitor<StringBuilder> getExpressionVisitor() {
5458
return expressionVisitor;
5559
}

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

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,8 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
687687
else {
688688
// Handle unnamed tokens by image
689689
String img = t.image;
690+
// DuckDB's LIMIT 10%: the % ends the row count instead of starting a modulo
691+
if (t == limitPercentToken) break;
690692
if ("*".equals(img) || "%".equals(img)) prec = 6;
691693
else if ("+".equals(img) || "-".equals(img) || "&".equals(img)) prec = 5;
692694
else break; // not an arithmetic operator
@@ -1530,6 +1532,51 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15301532
}
15311533
}
15321534

1535+
// the % of DuckDB's LIMIT 10% in the LIMIT clause being parsed, which ends its row count
1536+
private Token limitPercentToken = null;
1537+
1538+
/**
1539+
* Finds the % that ends the row count of the LIMIT clause starting after the current token:
1540+
* at its top level and directly followed by the end of the clause, as in LIMIT 10% OFFSET 2.
1541+
*/
1542+
private Token findLimitPercentToken() {
1543+
int depth = 0;
1544+
for (Token token = nextWithItemToken(getToken(0));; token = nextWithItemToken(token)) {
1545+
if (token.kind == EOF || token.kind == ST_SEMICOLON) {
1546+
return null;
1547+
}
1548+
if ("(".equals(token.image) || "[".equals(token.image)) {
1549+
depth++;
1550+
} else if (")".equals(token.image) || "]".equals(token.image)) {
1551+
if (--depth < 0) {
1552+
return null;
1553+
}
1554+
} else if (depth == 0 && "%".equals(token.image)
1555+
&& isLimitClauseEnd(nextWithItemToken(token))) {
1556+
return token;
1557+
}
1558+
}
1559+
}
1560+
1561+
/** Whether the token ends a LIMIT clause; OFFSET and FETCH only when they start a clause. */
1562+
private boolean isLimitClauseEnd(Token token) {
1563+
switch (token.kind) {
1564+
case EOF: case ST_SEMICOLON: case CLOSING_BRACKET:
1565+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1566+
return true;
1567+
case K_OFFSET:
1568+
// not a column named offset, as in LIMIT a % offset + 1
1569+
Token value = nextWithItemToken(token);
1570+
return value.kind == S_LONG || value.kind == OPENING_BRACKET
1571+
|| "?".equals(value.image) || ":".equals(value.image);
1572+
case K_FETCH:
1573+
Token next = nextWithItemToken(token);
1574+
return next.kind == K_FIRST || next.kind == K_NEXT;
1575+
default:
1576+
return false;
1577+
}
1578+
}
1579+
15331580
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
15341581
private boolean isSampleMethod(Token token) {
15351582
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
@@ -7146,9 +7193,13 @@ SampleClause SampleClause():
71467193
)
71477194
|
71487195
(
7149-
// SQL:2016 compliant
7196+
// SQL:2016 compliant; DuckDB: the method is optional and may be reservoir
71507197
token = <K_TABLESAMPLE> { keyword = token.image; }
7151-
( token = <K_SYSTEM> | token = <K_BERNOULLI> ) { method = token.image; }
7198+
[
7199+
LOOKAHEAD({ isSampleMethod(getToken(1)) })
7200+
( token = <K_SYSTEM> | token = <K_BERNOULLI> | token = <S_IDENTIFIER> )
7201+
{ method = token.image; }
7202+
]
71527203
)
71537204
|
71547205
(
@@ -7189,7 +7240,8 @@ SampleClause SampleClause():
71897240
<K_ROWS> { percentageUnit="ROWS"; }
71907241
)
71917242
]
7192-
[ LOOKAHEAD(2) <K_OFFSET> offsetArgument = Number() ]
7243+
// ClickHouse's SAMPLE k OFFSET m; after DuckDB's TABLESAMPLE n, OFFSET belongs to the query
7244+
[ LOOKAHEAD(2, { !"TABLESAMPLE".equalsIgnoreCase(keyword) }) <K_OFFSET> offsetArgument = Number() ]
71937245
)
71947246

71957247
// DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed])
@@ -10311,11 +10363,17 @@ Limit LimitWithOffset() #LimitWithOffset:
1031110363
}
1031210364
{
1031310365
<K_LIMIT>
10314-
(
10315-
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10316-
|
10317-
firstExpression = Expression()
10318-
)
10366+
{ Token outerLimitPercentToken = limitPercentToken; }
10367+
try {
10368+
{ limitPercentToken = findLimitPercentToken(); }
10369+
(
10370+
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10371+
|
10372+
firstExpression = Expression()
10373+
)
10374+
} finally {
10375+
limitPercentToken = outerLimitPercentToken;
10376+
}
1031910377
(
1032010378
// MySQL: LIMIT offset, row_count
1032110379
","
@@ -10325,10 +10383,15 @@ Limit LimitWithOffset() #LimitWithOffset:
1032510383
limit.setRowCount(secondExpression);
1032610384
}
1032710385
|
10328-
// PostgreSQL: LIMIT row_count
10386+
// PostgreSQL: LIMIT row_count; DuckDB: LIMIT 10% or LIMIT 10 PERCENT
1032910387
{
1033010388
limit.setRowCount(firstExpression);
1033110389
}
10390+
[
10391+
LOOKAHEAD({ getToken(1).kind == K_PERCENT
10392+
|| "%".equals(getToken(1).image) && isLimitClauseEnd(getToken(2)) })
10393+
( "%" { limit.setPercentUnit("%"); } | <K_PERCENT> { limit.setPercentUnit("PERCENT"); } )
10394+
]
1033210395
)
1033310396
{
1033410397
linkAST(limit, jjtThis);

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,4 +865,42 @@ void testTableQueryKeepsTableColumnsAndTrailingClauses() throws JSQLParserExcept
865865
Assertions.assertInstanceOf(net.sf.jsqlparser.statement.piped.FromQuery.class,
866866
((ParenthesedSelect) CCJSqlParserUtil.parse("(FROM t)")).getSelect());
867867
}
868+
869+
@Test
870+
void testLimitPercent() throws JSQLParserException {
871+
for (String sqlStr : new String[] {"SELECT a FROM t LIMIT 10%",
872+
"SELECT a FROM t LIMIT 10.5%", "SELECT a FROM t LIMIT 40 PERCENT",
873+
"SELECT a FROM t LIMIT 50% OFFSET 2", "SELECT a FROM t LIMIT (SELECT 10)%",
874+
"SELECT * FROM (SELECT a FROM t LIMIT 10%) x",
875+
"SELECT a FROM t LIMIT (SELECT a FROM u LIMIT 5%)", "SELECT a FROM t LIMIT NULL%",
876+
// modulo and a column named offset are unchanged
877+
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset",
878+
"SELECT a FROM t LIMIT a % offset + 1", "SELECT a FROM t LIMIT a % offset OFFSET 2",
879+
"SELECT a FROM t LIMIT (SELECT a % offset + 1 FROM u)"}) {
880+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
881+
}
882+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%");
883+
Assertions.assertEquals("%", select.getLimit().getPercentUnit());
884+
Assertions.assertEquals("10", select.getLimit().getRowCount().toString());
885+
886+
select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT a % offset + 1");
887+
Assertions.assertNull(select.getLimit().getPercentUnit());
888+
Assertions.assertNull(select.getOffset());
889+
}
890+
891+
@Test
892+
void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException {
893+
for (String sqlStr : new String[] {"SELECT a FROM t TABLESAMPLE 10%",
894+
"SELECT a FROM t TABLESAMPLE 10 ROWS", "SELECT a FROM t t1 TABLESAMPLE 10 PERCENT",
895+
"SELECT a FROM t TABLESAMPLE RESERVOIR (10 ROWS) REPEATABLE (1)",
896+
"SELECT a FROM (SELECT 1) TABLESAMPLE 10%",
897+
"SELECT a FROM read_csv('x') TABLESAMPLE 10%",
898+
"SELECT a FROM range(10) r USING SAMPLE 10%"}) {
899+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
900+
}
901+
// after DuckDB's TABLESAMPLE n, OFFSET belongs to the query
902+
PlainSelect select = (PlainSelect) CCJSqlParserUtil
903+
.parse("SELECT a FROM t TABLESAMPLE 10 OFFSET 3");
904+
Assertions.assertEquals("3", select.getOffset().getOffset().toString());
905+
}
868906
}

0 commit comments

Comments
 (0)