Skip to content

Commit 4566175

Browse files
committed
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 7dd714b commit 4566175

4 files changed

Lines changed: 73 additions & 33 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public String toString() {
100100

101101
if (rowCount instanceof AllValue || rowCount instanceof NullValue) {
102102
// no offset allowed
103-
retVal += rowCount;
103+
retVal += rowCount + percentUnitToString();
104104
} else {
105105
if (null != offset) {
106106
retVal += offset + ", ";

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +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-
}
34+
if (null != limit.getOffset()) {
35+
limit.getOffset().accept(expressionVisitor, context);
36+
builder.append(", ");
37+
}
4038

41-
if (null != limit.getRowCount()) {
42-
limit.getRowCount().accept(expressionVisitor, context);
43-
if (limit.getPercentUnit() != null) {
44-
builder.append("%".equals(limit.getPercentUnit()) ? "%"
45-
: " " + limit.getPercentUnit());
46-
}
47-
}
39+
if (null != limit.getRowCount()) {
40+
limit.getRowCount().accept(expressionVisitor, context);
41+
appendPercentUnit(limit);
4842
}
4943
}
5044

@@ -54,6 +48,12 @@ public <S> void deParse(Limit limit, S context) {
5448
}
5549
}
5650

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

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

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
688688
// Handle unnamed tokens by image
689689
String img = t.image;
690690
// DuckDB's LIMIT 10%: the % ends the row count instead of starting a modulo
691-
if ("%".equals(img) && limitRowCountDepth > 0 && isLimitPercentEndAt(2)) break;
691+
if (t == limitPercentToken) break;
692692
if ("*".equals(img) || "%".equals(img)) prec = 6;
693693
else if ("+".equals(img) || "-".equals(img) || "&".equals(img)) prec = 5;
694694
else break; // not an arithmetic operator
@@ -1532,25 +1532,52 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15321532
}
15331533
}
15341534

1535-
/** 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;
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+
}
15381560

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) {
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) {
15421564
case EOF: case ST_SEMICOLON: case CLOSING_BRACKET:
15431565
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
15441566
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;
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;
15491575
default:
15501576
return false;
15511577
}
15521578
}
15531579

1580+
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
15541581
private boolean isSampleMethod(Token token) {
15551582
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
15561583
|| (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image));
@@ -7213,7 +7240,8 @@ SampleClause SampleClause():
72137240
<K_ROWS> { percentageUnit="ROWS"; }
72147241
)
72157242
]
7216-
[ 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() ]
72177245
)
72187246

72197247
// DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed])
@@ -10335,15 +10363,16 @@ Limit LimitWithOffset() #LimitWithOffset:
1033510363
}
1033610364
{
1033710365
<K_LIMIT>
10366+
{ Token outerLimitPercentToken = limitPercentToken; }
1033810367
try {
10339-
{ limitRowCountDepth++; }
10368+
{ limitPercentToken = findLimitPercentToken(); }
1034010369
(
1034110370
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
1034210371
|
1034310372
firstExpression = Expression()
1034410373
)
1034510374
} finally {
10346-
limitRowCountDepth--;
10375+
limitPercentToken = outerLimitPercentToken;
1034710376
}
1034810377
(
1034910378
// MySQL: LIMIT offset, row_count
@@ -10360,7 +10389,7 @@ Limit LimitWithOffset() #LimitWithOffset:
1036010389
}
1036110390
[
1036210391
LOOKAHEAD({ getToken(1).kind == K_PERCENT
10363-
|| "%".equals(getToken(1).image) && isLimitPercentEndAt(2) })
10392+
|| "%".equals(getToken(1).image) && isLimitClauseEnd(getToken(2)) })
1036410393
( "%" { limit.setPercentUnit("%"); } | <K_PERCENT> { limit.setPercentUnit("PERCENT"); } )
1036510394
]
1036610395
)

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,13 +872,20 @@ void testLimitPercent() throws JSQLParserException {
872872
"SELECT a FROM t LIMIT 10.5%", "SELECT a FROM t LIMIT 40 PERCENT",
873873
"SELECT a FROM t LIMIT 50% OFFSET 2", "SELECT a FROM t LIMIT (SELECT 10)%",
874874
"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%",
875876
// modulo and a column named offset are unchanged
876-
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset"}) {
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)"}) {
877880
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
878881
}
879882
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%");
880883
Assertions.assertEquals("%", select.getLimit().getPercentUnit());
881884
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());
882889
}
883890

884891
@Test
@@ -891,5 +898,9 @@ void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException {
891898
"SELECT a FROM range(10) r USING SAMPLE 10%"}) {
892899
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
893900
}
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());
894905
}
895906
}

0 commit comments

Comments
 (0)