Skip to content

Commit f26ed7f

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 66fb21b commit f26ed7f

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
@@ -1500,25 +1500,52 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15001500
}
15011501
}
15021502

1503-
/** 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;
1503+
// the % of DuckDB's LIMIT 10% in the LIMIT clause being parsed, which ends its row count
1504+
private Token limitPercentToken = null;
1505+
1506+
/**
1507+
* Finds the % that ends the row count of the LIMIT clause starting after the current token:
1508+
* at its top level and directly followed by the end of the clause, as in LIMIT 10% OFFSET 2.
1509+
*/
1510+
private Token findLimitPercentToken() {
1511+
int depth = 0;
1512+
for (Token token = nextWithItemToken(getToken(0));; token = nextWithItemToken(token)) {
1513+
if (token.kind == EOF || token.kind == ST_SEMICOLON) {
1514+
return null;
1515+
}
1516+
if ("(".equals(token.image) || "[".equals(token.image)) {
1517+
depth++;
1518+
} else if (")".equals(token.image) || "]".equals(token.image)) {
1519+
if (--depth < 0) {
1520+
return null;
1521+
}
1522+
} else if (depth == 0 && "%".equals(token.image)
1523+
&& isLimitClauseEnd(nextWithItemToken(token))) {
1524+
return token;
1525+
}
1526+
}
1527+
}
15061528

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) {
1529+
/** Whether the token ends a LIMIT clause; OFFSET and FETCH only when they start a clause. */
1530+
private boolean isLimitClauseEnd(Token token) {
1531+
switch (token.kind) {
15101532
case EOF: case ST_SEMICOLON: case CLOSING_BRACKET:
15111533
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
15121534
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;
1535+
case K_OFFSET:
1536+
// not a column named offset, as in LIMIT a % offset + 1
1537+
Token value = nextWithItemToken(token);
1538+
return value.kind == S_LONG || value.kind == OPENING_BRACKET
1539+
|| "?".equals(value.image) || ":".equals(value.image);
1540+
case K_FETCH:
1541+
Token next = nextWithItemToken(token);
1542+
return next.kind == K_FIRST || next.kind == K_NEXT;
15171543
default:
15181544
return false;
15191545
}
15201546
}
15211547

1548+
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
15221549
private boolean isSampleMethod(Token token) {
15231550
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
15241551
|| (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image));
@@ -7146,7 +7173,8 @@ SampleClause SampleClause():
71467173
<K_ROWS> { percentageUnit="ROWS"; }
71477174
)
71487175
]
7149-
[ LOOKAHEAD(2) <K_OFFSET> offsetArgument = Number() ]
7176+
// ClickHouse's SAMPLE k OFFSET m; after DuckDB's TABLESAMPLE n, OFFSET belongs to the query
7177+
[ LOOKAHEAD(2, { !"TABLESAMPLE".equalsIgnoreCase(keyword) }) <K_OFFSET> offsetArgument = Number() ]
71507178
)
71517179

71527180
// DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed])
@@ -10247,15 +10275,16 @@ Limit LimitWithOffset() #LimitWithOffset:
1024710275
}
1024810276
{
1024910277
<K_LIMIT>
10278+
{ Token outerLimitPercentToken = limitPercentToken; }
1025010279
try {
10251-
{ limitRowCountDepth++; }
10280+
{ limitPercentToken = findLimitPercentToken(); }
1025210281
(
1025310282
LOOKAHEAD(3) firstExpression = ParenthesedSelect()
1025410283
|
1025510284
firstExpression = Expression()
1025610285
)
1025710286
} finally {
10258-
limitRowCountDepth--;
10287+
limitPercentToken = outerLimitPercentToken;
1025910288
}
1026010289
(
1026110290
// MySQL: LIMIT offset, row_count
@@ -10272,7 +10301,7 @@ Limit LimitWithOffset() #LimitWithOffset:
1027210301
}
1027310302
[
1027410303
LOOKAHEAD({ getToken(1).kind == K_PERCENT
10275-
|| "%".equals(getToken(1).image) && isLimitPercentEndAt(2) })
10304+
|| "%".equals(getToken(1).image) && isLimitClauseEnd(getToken(2)) })
1027610305
( "%" { limit.setPercentUnit("%"); } | <K_PERCENT> { limit.setPercentUnit("PERCENT"); } )
1027710306
]
1027810307
)

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,13 +832,20 @@ void testLimitPercent() throws JSQLParserException {
832832
"SELECT a FROM t LIMIT 10.5%", "SELECT a FROM t LIMIT 40 PERCENT",
833833
"SELECT a FROM t LIMIT 50% OFFSET 2", "SELECT a FROM t LIMIT (SELECT 10)%",
834834
"SELECT * FROM (SELECT a FROM t LIMIT 10%) x",
835+
"SELECT a FROM t LIMIT (SELECT a FROM u LIMIT 5%)", "SELECT a FROM t LIMIT NULL%",
835836
// modulo and a column named offset are unchanged
836-
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset"}) {
837+
"SELECT a FROM t LIMIT 10 % 3", "SELECT a FROM t LIMIT a % offset",
838+
"SELECT a FROM t LIMIT a % offset + 1", "SELECT a FROM t LIMIT a % offset OFFSET 2",
839+
"SELECT a FROM t LIMIT (SELECT a % offset + 1 FROM u)"}) {
837840
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
838841
}
839842
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT 10%");
840843
Assertions.assertEquals("%", select.getLimit().getPercentUnit());
841844
Assertions.assertEquals("10", select.getLimit().getRowCount().toString());
845+
846+
select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a FROM t LIMIT a % offset + 1");
847+
Assertions.assertNull(select.getLimit().getPercentUnit());
848+
Assertions.assertNull(select.getOffset());
842849
}
843850

844851
@Test
@@ -851,5 +858,9 @@ void testTableSampleWithoutOrWithReservoirMethod() throws JSQLParserException {
851858
"SELECT a FROM range(10) r USING SAMPLE 10%"}) {
852859
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
853860
}
861+
// after DuckDB's TABLESAMPLE n, OFFSET belongs to the query
862+
PlainSelect select = (PlainSelect) CCJSqlParserUtil
863+
.parse("SELECT a FROM t TABLESAMPLE 10 OFFSET 3");
864+
Assertions.assertEquals("3", select.getOffset().getOffset().toString());
854865
}
855866
}

0 commit comments

Comments
 (0)