Skip to content

Commit 34c97e4

Browse files
hayssamsclaude
andcommitted
Support DuckDB USING SAMPLE sizes and methods
DuckDB writes the sample size first and the method behind it: USING SAMPLE 10%, USING SAMPLE 10 PERCENT (bernoulli), USING SAMPLE 10 ROWS (system, 377), USING SAMPLE reservoir (50 ROWS). The method is now optional, RESERVOIR is a known method, and a bare sample size accepts a unit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent d7f5fe2 commit 34c97e4

3 files changed

Lines changed: 96 additions & 6 deletions

File tree

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class SampleClause {
1717
private Number percentageArgument;
1818
private String percentageUnit;
1919
private boolean argumentInBrackets = true;
20+
private boolean methodInBrackets = false;
2021
// ClickHouse specific
2122
private Number offsetArgument;
2223
private Number repeatArgument;
@@ -130,10 +131,23 @@ public SampleClause setMethod(String method) {
130131
return this;
131132
}
132133

134+
/**
135+
* DuckDB writes the sampling method behind the sample size:
136+
* {@code USING SAMPLE 10 PERCENT (bernoulli)}.
137+
*/
138+
public boolean isMethodInBrackets() {
139+
return methodInBrackets;
140+
}
141+
142+
public SampleClause setMethodInBrackets(boolean methodInBrackets) {
143+
this.methodInBrackets = methodInBrackets;
144+
return this;
145+
}
146+
133147
public StringBuilder appendTo(StringBuilder builder) {
134148

135149
builder.append(" ").append(keyword);
136-
if (method != null) {
150+
if (method != null && !methodInBrackets) {
137151
builder.append(" ").append(method);
138152
}
139153

@@ -144,11 +158,20 @@ public StringBuilder appendTo(StringBuilder builder) {
144158
} else {
145159
builder.append(" ").append(percentageArgument);
146160
if (percentageUnit != null) {
147-
builder.append(" ").append(percentageUnit);
161+
// "10%" has no blank, "10 PERCENT" and "10 ROWS" do
162+
builder.append("%".equals(percentageUnit) ? "" : " ").append(percentageUnit);
148163
}
149164
}
150165
}
151166

167+
if (methodInBrackets) {
168+
builder.append(" (").append(method);
169+
if (seedArgument != null) {
170+
builder.append(", ").append(seedArgument);
171+
}
172+
builder.append(")");
173+
}
174+
152175
if (offsetArgument != null) {
153176
builder.append(" OFFSET ").append(offsetArgument);
154177
}
@@ -157,7 +180,7 @@ public StringBuilder appendTo(StringBuilder builder) {
157180
builder.append(" REPEATABLE (").append(repeatArgument).append(")");
158181
}
159182

160-
if (seedArgument != null) {
183+
if (seedArgument != null && !methodInBrackets) {
161184
builder.append(" SEED (").append(seedArgument).append(")");
162185
}
163186

@@ -189,7 +212,7 @@ public String toString() {
189212
}
190213

191214
public enum SampleMethod {
192-
BERNOULLI, SYSTEM, BLOCK;
215+
BERNOULLI, SYSTEM, BLOCK, RESERVOIR;
193216

194217
public static SampleMethod from(String sampleMethod) {
195218
return Enum.valueOf(SampleMethod.class,

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12111211
return isReservedKeywordSafeByFollower();
12121212
}
12131213

1214+
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
1215+
private boolean isSampleMethod(Token token) {
1216+
return token.kind == K_SYSTEM || token.kind == K_BERNOULLI
1217+
|| (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image));
1218+
}
1219+
12141220
/** DuckDB's INSERT ... BY NAME|POSITION column matching, never a table alias. */
12151221
private boolean isColumnMatchingAhead() {
12161222
return getToken(1).kind == K_BY
@@ -5871,6 +5877,7 @@ SampleClause SampleClause():
58715877
Number offsetArgument = null;
58725878
Number repeatArgument=null;
58735879
Number seedArgument=null;
5880+
boolean methodInBrackets = false;
58745881
}
58755882
{
58765883
(
@@ -5887,9 +5894,13 @@ SampleClause SampleClause():
58875894
)
58885895
|
58895896
(
5890-
// Duck DB
5897+
// Duck DB: the method is optional and may follow the sample size instead
58915898
<K_USING> <K_SAMPLE> { keyword = "USING SAMPLE"; }
5892-
( token = <K_SYSTEM> | token = <K_BERNOULLI> ) { method = token.image; }
5899+
[
5900+
LOOKAHEAD({ isSampleMethod(getToken(1)) })
5901+
( token = <K_SYSTEM> | token = <K_BERNOULLI> | token = <S_IDENTIFIER> )
5902+
{ method = token.image; }
5903+
]
58935904
)
58945905
)
58955906

@@ -5909,12 +5920,34 @@ SampleClause SampleClause():
59095920
[ LOOKAHEAD(2) <K_SEED> "(" seedArgument = Number() ")" ]
59105921
|
59115922
percentageArgument = Number() { argumentInBrackets = false; }
5923+
// greedy: a unit directly after the sample size always belongs to the clause
5924+
[
5925+
LOOKAHEAD(1)
5926+
(
5927+
"%" { percentageUnit="%"; }
5928+
|
5929+
<K_PERCENT> { percentageUnit="PERCENT"; }
5930+
|
5931+
<K_ROWS> { percentageUnit="ROWS"; }
5932+
)
5933+
]
59125934
[ LOOKAHEAD(2) <K_OFFSET> offsetArgument = Number() ]
59135935
)
59145936

5937+
// DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed])
5938+
[
5939+
LOOKAHEAD({ method == null && getToken(1).kind == OPENING_BRACKET
5940+
&& isSampleMethod(getToken(2)) })
5941+
"(" ( token = <K_SYSTEM> | token = <K_BERNOULLI> | token = <S_IDENTIFIER> )
5942+
{ method = token.image; methodInBrackets = true; }
5943+
[ "," seedArgument = Number() ]
5944+
")"
5945+
]
5946+
59155947
{
59165948
sampleClause = new SampleClause(keyword, method, percentageArgument, percentageUnit, repeatArgument, seedArgument);
59175949
sampleClause.setArgumentInBrackets(argumentInBrackets);
5950+
sampleClause.setMethodInBrackets(methodInBrackets);
59185951
sampleClause.setOffsetArgument(offsetArgument);
59195952
return sampleClause;
59205953
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,38 @@ void testInsertWithoutColumnMatching() throws JSQLParserException {
142142

143143
Assertions.assertNull(insert.getColumnMatching());
144144
}
145+
146+
@Test
147+
void testUsingSamplePercentShorthand() throws JSQLParserException {
148+
String sqlStr = "SELECT * FROM t USING SAMPLE 10%";
149+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
150+
SampleClause sampleClause = select.getFromItem().getSampleClause();
151+
152+
Assertions.assertEquals("%", sampleClause.getPercentageUnit());
153+
Assertions.assertNull(sampleClause.getMethod());
154+
}
155+
156+
@Test
157+
void testUsingSamplePercentWithMethodInBrackets() throws JSQLParserException {
158+
String sqlStr = "SELECT * FROM t USING SAMPLE 10 PERCENT (bernoulli)";
159+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
160+
161+
Assertions.assertEquals(SampleClause.SampleMethod.BERNOULLI,
162+
select.getFromItem().getSampleClause().getMethod());
163+
}
164+
165+
@Test
166+
void testUsingSampleRowsWithSeed() throws JSQLParserException {
167+
String sqlStr = "SELECT * FROM t USING SAMPLE 10 ROWS (system, 377)";
168+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
169+
}
170+
171+
@Test
172+
void testUsingSampleReservoir() throws JSQLParserException {
173+
String sqlStr = "SELECT * FROM t USING SAMPLE reservoir (50 ROWS)";
174+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
175+
176+
Assertions.assertEquals(SampleClause.SampleMethod.RESERVOIR,
177+
select.getFromItem().getSampleClause().getMethod());
178+
}
145179
}

0 commit comments

Comments
 (0)