Skip to content

Commit 66fb21b

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 19f12ea commit 66fb21b

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
@@ -1499,6 +1501,24 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14991501
}
15001502

15011503
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
1504+
// depth of LIMIT row counts, where a % before the end of the clause is DuckDB's percent unit
1505+
private int limitRowCountDepth = 0;
1506+
1507+
/** Whether the token at {@code index} ends a LIMIT clause, after DuckDB's LIMIT 10%. */
1508+
private boolean isLimitPercentEndAt(int index) {
1509+
switch (getToken(index).kind) {
1510+
case EOF: case ST_SEMICOLON: case CLOSING_BRACKET:
1511+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1512+
return true;
1513+
case K_OFFSET: case K_FETCH:
1514+
// not a column named offset: LIMIT a % offset
1515+
int next = getToken(index + 1).kind;
1516+
return next != EOF && next != ST_SEMICOLON && next != CLOSING_BRACKET;
1517+
default:
1518+
return false;
1519+
}
1520+
}
1521+
15021522
private boolean isSampleMethod(Token token) {
15031523
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
15041524
|| (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image));
@@ -7079,9 +7099,13 @@ SampleClause SampleClause():
70797099
)
70807100
|
70817101
(
7082-
// SQL:2016 compliant
7102+
// SQL:2016 compliant; DuckDB: the method is optional and may be reservoir
70837103
token = <K_TABLESAMPLE> { keyword = token.image; }
7084-
( token = <K_SYSTEM> | token = <K_BERNOULLI> ) { method = token.image; }
7104+
[
7105+
LOOKAHEAD({ isSampleMethod(getToken(1)) })
7106+
( token = <K_SYSTEM> | token = <K_BERNOULLI> | token = <S_IDENTIFIER> )
7107+
{ method = token.image; }
7108+
]
70857109
)
70867110
|
70877111
(
@@ -10223,11 +10247,16 @@ Limit LimitWithOffset() #LimitWithOffset:
1022310247
}
1022410248
{
1022510249
<K_LIMIT>
10226-
(
10227-
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10228-
|
10229-
firstExpression = Expression()
10230-
)
10250+
try {
10251+
{ limitRowCountDepth++; }
10252+
(
10253+
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
10254+
|
10255+
firstExpression = Expression()
10256+
)
10257+
} finally {
10258+
limitRowCountDepth--;
10259+
}
1023110260
(
1023210261
// MySQL: LIMIT offset, row_count
1023310262
","
@@ -10237,10 +10266,15 @@ Limit LimitWithOffset() #LimitWithOffset:
1023710266
limit.setRowCount(secondExpression);
1023810267
}
1023910268
|
10240-
// PostgreSQL: LIMIT row_count
10269+
// PostgreSQL: LIMIT row_count; DuckDB: LIMIT 10% or LIMIT 10 PERCENT
1024110270
{
1024210271
limit.setRowCount(firstExpression);
1024310272
}
10273+
[
10274+
LOOKAHEAD({ getToken(1).kind == K_PERCENT
10275+
|| "%".equals(getToken(1).image) && isLimitPercentEndAt(2) })
10276+
( "%" { limit.setPercentUnit("%"); } | <K_PERCENT> { limit.setPercentUnit("PERCENT"); } )
10277+
]
1024410278
)
1024510279
{
1024610280
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
@@ -825,4 +825,31 @@ 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 testLimitPercent() throws JSQLParserException {
831+
for (String sqlStr : new String[] {"SELECT a FROM t LIMIT 10%",
832+
"SELECT a FROM t LIMIT 10.5%", "SELECT a FROM t LIMIT 40 PERCENT",
833+
"SELECT a FROM t LIMIT 50% OFFSET 2", "SELECT a FROM t LIMIT (SELECT 10)%",
834+
"SELECT * FROM (SELECT a FROM t LIMIT 10%) x",
835+
// modulo and a column named offset are unchanged
836+
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset"}) {
837+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
838+
}
839+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%");
840+
Assertions.assertEquals("%", select.getLimit().getPercentUnit());
841+
Assertions.assertEquals("10", select.getLimit().getRowCount().toString());
842+
}
843+
844+
@Test
845+
void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException {
846+
for (String sqlStr : new String[] {"SELECT a FROM t TABLESAMPLE 10%",
847+
"SELECT a FROM t TABLESAMPLE 10 ROWS", "SELECT a FROM t t1 TABLESAMPLE 10 PERCENT",
848+
"SELECT a FROM t TABLESAMPLE RESERVOIR (10 ROWS) REPEATABLE (1)",
849+
"SELECT a FROM (SELECT 1) TABLESAMPLE 10%",
850+
"SELECT a FROM read_csv('x') TABLESAMPLE 10%",
851+
"SELECT a FROM range(10) r USING SAMPLE 10%"}) {
852+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
853+
}
854+
}
828855
}

0 commit comments

Comments
 (0)