Skip to content

Commit 7dd714b

Browse files
committed
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.
1 parent ac8a4f0 commit 7dd714b

5 files changed

Lines changed: 105 additions & 12 deletions

File tree

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

Lines changed: 23 additions & 1 deletion
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,6 +74,26 @@ 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 ";
@@ -84,7 +106,7 @@ public String toString() {
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public <S> void deParse(Limit limit, S context) {
4040

4141
if (null != limit.getRowCount()) {
4242
limit.getRowCount().accept(expressionVisitor, context);
43+
if (limit.getPercentUnit() != null) {
44+
builder.append("%".equals(limit.getPercentUnit()) ? "%"
45+
: " " + limit.getPercentUnit());
46+
}
4347
}
4448
}
4549
}

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

Lines changed: 42 additions & 8 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 ("%".equals(img) && limitRowCountDepth > 0 && isLimitPercentEndAt(2)) 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
@@ -1531,6 +1533,24 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15311533
}
15321534

15331535
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
1536+
// depth of LIMIT row counts, where a % before the end of the clause is DuckDB's percent unit
1537+
private int limitRowCountDepth = 0;
1538+
1539+
/** Whether the token at {@code index} ends a LIMIT clause, after DuckDB's LIMIT 10%. */
1540+
private boolean isLimitPercentEndAt(int index) {
1541+
switch (getToken(index).kind) {
1542+
case EOF: case ST_SEMICOLON: case CLOSING_BRACKET:
1543+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1544+
return true;
1545+
case K_OFFSET: case K_FETCH:
1546+
// not a column named offset: LIMIT a % offset
1547+
int next = getToken(index + 1).kind;
1548+
return next != EOF && next != ST_SEMICOLON && next != CLOSING_BRACKET;
1549+
default:
1550+
return false;
1551+
}
1552+
}
1553+
15341554
private boolean isSampleMethod(Token token) {
15351555
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
15361556
|| (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image));
@@ -7146,9 +7166,13 @@ SampleClause SampleClause():
71467166
)
71477167
|
71487168
(
7149-
// SQL:2016 compliant
7169+
// SQL:2016 compliant; DuckDB: the method is optional and may be reservoir
71507170
token = <K_TABLESAMPLE> { keyword = token.image; }
7151-
( token = <K_SYSTEM> | token = <K_BERNOULLI> ) { method = token.image; }
7171+
[
7172+
LOOKAHEAD({ isSampleMethod(getToken(1)) })
7173+
( token = <K_SYSTEM> | token = <K_BERNOULLI> | token = <S_IDENTIFIER> )
7174+
{ method = token.image; }
7175+
]
71527176
)
71537177
|
71547178
(
@@ -10311,11 +10335,16 @@ Limit LimitWithOffset() #LimitWithOffset:
1031110335
}
1031210336
{
1031310337
<K_LIMIT>
10314-
(
10315-
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10316-
|
10317-
firstExpression = Expression()
10318-
)
10338+
try {
10339+
{ limitRowCountDepth++; }
10340+
(
10341+
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10342+
|
10343+
firstExpression = Expression()
10344+
)
10345+
} finally {
10346+
limitRowCountDepth--;
10347+
}
1031910348
(
1032010349
// MySQL: LIMIT offset, row_count
1032110350
","
@@ -10325,10 +10354,15 @@ Limit LimitWithOffset() #LimitWithOffset:
1032510354
limit.setRowCount(secondExpression);
1032610355
}
1032710356
|
10328-
// PostgreSQL: LIMIT row_count
10357+
// PostgreSQL: LIMIT row_count; DuckDB: LIMIT 10% or LIMIT 10 PERCENT
1032910358
{
1033010359
limit.setRowCount(firstExpression);
1033110360
}
10361+
[
10362+
LOOKAHEAD({ getToken(1).kind == K_PERCENT
10363+
|| "%".equals(getToken(1).image) && isLimitPercentEndAt(2) })
10364+
( "%" { limit.setPercentUnit("%"); } | <K_PERCENT> { limit.setPercentUnit("PERCENT"); } )
10365+
]
1033210366
)
1033310367
{
1033410368
linkAST(limit, jjtThis);

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,4 +865,31 @@ 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+
// modulo and a column named offset are unchanged
876+
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset"}) {
877+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
878+
}
879+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%");
880+
Assertions.assertEquals("%", select.getLimit().getPercentUnit());
881+
Assertions.assertEquals("10", select.getLimit().getRowCount().toString());
882+
}
883+
884+
@Test
885+
void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException {
886+
for (String sqlStr : new String[] {"SELECT a FROM t TABLESAMPLE 10%",
887+
"SELECT a FROM t TABLESAMPLE 10 ROWS", "SELECT a FROM t t1 TABLESAMPLE 10 PERCENT",
888+
"SELECT a FROM t TABLESAMPLE RESERVOIR (10 ROWS) REPEATABLE (1)",
889+
"SELECT a FROM (SELECT 1) TABLESAMPLE 10%",
890+
"SELECT a FROM read_csv('x') TABLESAMPLE 10%",
891+
"SELECT a FROM range(10) r USING SAMPLE 10%"}) {
892+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
893+
}
894+
}
868895
}

0 commit comments

Comments
 (0)