Skip to content

Commit 1387354

Browse files
AnEmortalKidJan Monterrubio
andauthored
Support options for Explain (#996)
* visual * issue-995 * support verbose * postgres explain * tests * no text Co-authored-by: Jan Monterrubio <[email protected]>
1 parent 8c3076e commit 1387354

File tree

5 files changed

+247
-4
lines changed

5 files changed

+247
-4
lines changed

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,19 @@
414414
</plugins>
415415
</build>
416416
</profile>
417+
<profile>
418+
<!-- for building the parser with debug flags -->
419+
<id>skip.all</id>
420+
<activation>
421+
<activeByDefault>false</activeByDefault>
422+
</activation>
423+
<properties>
424+
<maven.test.skip>true</maven.test.skip>
425+
<checkstyle.skip>true</checkstyle.skip>
426+
<maven.javadoc.skip>true</maven.javadoc.skip>
427+
<license.skipUpdateLicense>true</license.skipUpdateLicense>
428+
</properties>
429+
</profile>
417430
</profiles>
418431

419432
<properties>

src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@
1111

1212
import net.sf.jsqlparser.statement.select.Select;
1313

14+
import java.util.LinkedHashMap;
15+
import java.util.stream.Collectors;
16+
17+
/**
18+
* An {@code EXPLAIN} statement
19+
*/
1420
public class ExplainStatement implements Statement {
1521

1622
private Select select;
23+
private LinkedHashMap<OptionType, Option> options;
1724

1825
public ExplainStatement(Select select) {
1926
this.select = select;
@@ -27,13 +34,79 @@ public void setStatement(Select select) {
2734
this.select = select;
2835
}
2936

37+
public LinkedHashMap<OptionType, Option> getOptions() {
38+
return options == null ? null : new LinkedHashMap<>(options);
39+
}
40+
41+
public void addOption(Option option) {
42+
if (options == null) {
43+
options = new LinkedHashMap<>();
44+
}
45+
46+
options.put(option.getType(), option);
47+
}
48+
49+
/**
50+
* Returns the first option that matches this optionType
51+
* @param optionType the option type to retrieve an Option for
52+
* @return an option of that type, or null. In case of duplicate options, the first found option will be returned.
53+
*/
54+
public Option getOption(OptionType optionType) {
55+
if (options == null) {
56+
return null;
57+
}
58+
return options.get(optionType);
59+
}
60+
3061
@Override
3162
public String toString() {
32-
return "EXPLAIN " + select.toString();
63+
StringBuilder statementBuilder = new StringBuilder("EXPLAIN");
64+
if (options != null) {
65+
statementBuilder.append(" ");
66+
statementBuilder.append(options.values().stream().map(Option::formatOption).collect(Collectors.joining(" ")));
67+
}
68+
69+
statementBuilder.append(" ");
70+
statementBuilder.append(select.toString());
71+
return statementBuilder.toString();
3372
}
3473

3574
@Override
3675
public void accept(StatementVisitor statementVisitor) {
3776
statementVisitor.visit(this);
3877
}
78+
79+
public enum OptionType {
80+
ANALYZE,
81+
VERBOSE,
82+
COSTS,
83+
BUFFERS,
84+
FORMAT
85+
}
86+
87+
public static class Option {
88+
89+
private final OptionType type;
90+
private String value;
91+
92+
public Option(OptionType type) {
93+
this.type = type;
94+
}
95+
96+
public OptionType getType() {
97+
return type;
98+
}
99+
100+
public String getValue() {
101+
return value;
102+
}
103+
104+
public void setValue(String value) {
105+
this.value = value;
106+
}
107+
108+
public String formatOption() {
109+
return type.name() + (value != null ? (" " + value) : "");
110+
}
111+
}
39112
}

src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package net.sf.jsqlparser.util.deparser;
1111

1212
import java.util.Iterator;
13+
import java.util.stream.Collectors;
14+
1315
import net.sf.jsqlparser.statement.Block;
1416
import net.sf.jsqlparser.statement.Commit;
1517
import net.sf.jsqlparser.statement.DeclareStatement;
@@ -265,6 +267,10 @@ public void visit(DescribeStatement describe) {
265267
@Override
266268
public void visit(ExplainStatement explain) {
267269
buffer.append("EXPLAIN ");
270+
if (explain.getOptions() != null) {
271+
buffer.append(explain.getOptions().values().stream().map(ExplainStatement.Option::formatOption).collect(Collectors.joining(" ")));
272+
buffer.append(" ");
273+
}
268274
explain.getStatement().accept(this);
269275
}
270276

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

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
122122
| <K_ALGORITHM: "ALGORITHM">
123123
| <K_ALL:"ALL">
124124
| <K_ALTER:"ALTER">
125+
| <K_ANALYZE:"ANALYZE">
125126
| <K_AND:"AND">
126127
| <K_AND_OPERATOR:"&&">
127128
| <K_ANY:"ANY">
@@ -136,6 +137,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
136137
| <K_BOTH:"BOTH">
137138
| <K_BY:"BY">
138139
| <K_CACHE: "CACHE">
140+
| <K_BUFFERS: "BUFFERS">
139141
| <K_BYTE: "BYTE">
140142
| <K_CALL : "CALL">
141143
| <K_CASCADE: "CASCADE">
@@ -153,6 +155,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
153155
| <K_COMMENT:"COMMENT">
154156
| <K_CONNECT:"CONNECT">
155157
| <K_CONSTRAINT:"CONSTRAINT">
158+
| <K_COSTS: "COSTS">
156159
| <K_CREATE:"CREATE">
157160
| <K_CROSS:"CROSS">
158161
| <K_CURRENT: "CURRENT">
@@ -192,6 +195,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
192195
| <K_FOR:"FOR">
193196
| <K_FORCE : "FORCE">
194197
| <K_FOREIGN:"FOREIGN">
198+
| <K_FORMAT:"FORMAT">
195199
| <K_FROM:"FROM">
196200
| <K_FULL:"FULL">
197201
| <K_FULLTEXT:"FULLTEXT">
@@ -220,6 +224,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
220224
| <K_IS:"IS">
221225
| <K_ISNULL:"ISNULL">
222226
| <K_JOIN:"JOIN">
227+
| <K_JSON:"JSON">
223228
| <K_KEEP:"KEEP">
224229
| <K_KEY:"KEY">
225230
| <K_FN:"FN">
@@ -256,6 +261,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
256261
| <K_NULLS: "NULLS">
257262
| <K_NOWAIT: "NOWAIT">
258263
| <K_OF:"OF">
264+
| <K_OFF:"OFF">
259265
| <K_OFFSET:"OFFSET">
260266
| <K_ON:"ON">
261267
| <K_ONLY:"ONLY">
@@ -332,6 +338,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
332338
| <K_VALUE:"VALUE">
333339
| <K_VALUES:"VALUES">
334340
| <K_VARYING:"VARYING">
341+
| <K_VERBOSE: "VERBOSE">
335342
| <K_VIEW:"VIEW">
336343
| <K_WAIT : "WAIT">
337344
| <K_WHEN:"WHEN">
@@ -341,6 +348,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
341348
| <K_WITHIN:"WITHIN">
342349
| <K_WITHOUT:"WITHOUT">
343350
| <K_XML:"XML">
351+
| <K_YAML:"YAML">
344352
| <K_ZONE:"ZONE">
345353
}
346354

@@ -635,13 +643,109 @@ DescribeStatement Describe(): {
635643

636644
ExplainStatement Explain(): {
637645
Select select;
646+
List<ExplainStatement.Option> options = null;
638647
} {
639-
<K_EXPLAIN> select = Select()
648+
<K_EXPLAIN>
649+
options=ExplainStatementOptions()
650+
select = Select()
640651
{
641-
return new ExplainStatement(select);
652+
ExplainStatement es = new ExplainStatement(select);
653+
if(options != null && !options.isEmpty()) {
654+
for(ExplainStatement.Option o : options) {
655+
es.addOption(o);
656+
}
657+
}
658+
return es;
642659
}
643660
}
644661

662+
/**
663+
* Postgres supports TRUE,ON,1,FALSE,OFF,0 as values
664+
*/
665+
String ExplainOptionBoolean():
666+
{
667+
Token tk = null;
668+
}
669+
{
670+
// intentionally not supporting 0,1 at the moment
671+
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )] // optional
672+
{
673+
return tk != null ? tk.image : null;
674+
}
675+
}
676+
677+
/**
678+
* The output format, which can be TEXT, XML, JSON, or YAML
679+
*/
680+
String ExplainFormatOption():
681+
{
682+
Token tk = null;
683+
}
684+
{
685+
// TODO support Text
686+
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )] // optional
687+
{
688+
return tk != null ? tk.image : null;
689+
}
690+
}
691+
692+
/**
693+
* Options for explain, see https://www.postgresql.org/docs/9.1/sql-explain.html
694+
*/
695+
List<ExplainStatement.Option> ExplainStatementOptions():
696+
{
697+
List<ExplainStatement.Option> options = new ArrayList<ExplainStatement.Option>();
698+
ExplainStatement.Option option = null;
699+
Token token = null;
700+
String value = null;
701+
}
702+
{
703+
(
704+
(<K_ANALYZE> value=ExplainOptionBoolean()
705+
{
706+
option = new ExplainStatement.Option(ExplainStatement.OptionType.ANALYZE);
707+
option.setValue(value);
708+
options.add(option);
709+
}
710+
)
711+
|
712+
(<K_BUFFERS> value=ExplainOptionBoolean()
713+
{
714+
option = new ExplainStatement.Option(ExplainStatement.OptionType.BUFFERS);
715+
option.setValue(value);
716+
options.add(option);
717+
}
718+
)
719+
|
720+
(<K_COSTS> value=ExplainOptionBoolean()
721+
{
722+
option = new ExplainStatement.Option(ExplainStatement.OptionType.COSTS);
723+
option.setValue(value);
724+
options.add(option);
725+
}
726+
)
727+
|
728+
(<K_VERBOSE> value=ExplainOptionBoolean()
729+
{
730+
option = new ExplainStatement.Option(ExplainStatement.OptionType.VERBOSE);
731+
option.setValue(value);
732+
options.add(option);
733+
}
734+
)
735+
|
736+
(<K_FORMAT> value=ExplainFormatOption()
737+
{
738+
option = new ExplainStatement.Option(ExplainStatement.OptionType.FORMAT);
739+
option.setValue(value);
740+
options.add(option);
741+
}
742+
)
743+
)* //zero or many times those productions
744+
{
745+
return options;
746+
}
747+
}
748+
645749
UseStatement Use(): {
646750
String name;
647751
}
@@ -1175,7 +1279,7 @@ String RelObjectNameWithoutValue() :
11751279
| tk=<K_DATE_LITERAL> | tk=<K_NEXTVAL> | tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_DUPLICATE>
11761280
| tk=<K_READ> | tk=<K_SCHEMA> | tk=<K_SIZE> | tk=<K_SESSION>
11771281
| tk=<K_VIEW> | tk=<K_NOLOCK> | tk=<K_VALIDATE> | tk=<K_CYCLE>
1178-
/* | tk=<K_PLACING> | tk=<K_BOTH> | tk=<K_LEADING> | tk=<K_TRAILING> */
1282+
/*| tk=<K_PLACING> | tk=<K_BOTH> | tk=<K_LEADING> | tk=<K_TRAILING> */
11791283
)
11801284

11811285
{ return tk.image; }

src/test/java/net/sf/jsqlparser/statement/ExplainTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
import net.sf.jsqlparser.JSQLParserException;
1313
import static net.sf.jsqlparser.test.TestUtils.*;
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1417
import org.junit.Test;
1518

1619
public class ExplainTest {
@@ -19,4 +22,48 @@ public class ExplainTest {
1922
public void testDescribe() throws JSQLParserException {
2023
assertSqlCanBeParsedAndDeparsed("EXPLAIN SELECT * FROM mytable");
2124
}
25+
26+
@Test
27+
public void testAnalyze() throws JSQLParserException {
28+
assertSqlCanBeParsedAndDeparsed("EXPLAIN ANALYZE SELECT * FROM mytable");
29+
}
30+
31+
@Test
32+
public void testBuffers() throws JSQLParserException {
33+
assertSqlCanBeParsedAndDeparsed("EXPLAIN BUFFERS SELECT * FROM mytable");
34+
}
35+
36+
@Test
37+
public void testCosts() throws JSQLParserException {
38+
assertSqlCanBeParsedAndDeparsed("EXPLAIN COSTS SELECT * FROM mytable");
39+
}
40+
41+
@Test
42+
public void testFormat() throws JSQLParserException {
43+
assertSqlCanBeParsedAndDeparsed("EXPLAIN FORMAT XML SELECT * FROM mytable");
44+
}
45+
46+
@Test
47+
public void testVerbose() throws JSQLParserException {
48+
assertSqlCanBeParsedAndDeparsed("EXPLAIN VERBOSE SELECT * FROM mytable");
49+
}
50+
51+
@Test
52+
public void testMultiOptions_orderPreserved() throws JSQLParserException {
53+
assertSqlCanBeParsedAndDeparsed("EXPLAIN VERBOSE ANALYZE BUFFERS COSTS SELECT * FROM mytable");
54+
}
55+
56+
@Test
57+
public void getOption_returnsValues() throws JSQLParserException {
58+
ExplainStatement explain = (ExplainStatement) CCJSqlParserUtil.parse("EXPLAIN VERBOSE FORMAT JSON BUFFERS FALSE SELECT * FROM mytable");
59+
60+
assertThat(explain.getOption(ExplainStatement.OptionType.ANALYZE)).isNull();
61+
assertThat(explain.getOption(ExplainStatement.OptionType.VERBOSE)).isNotNull();
62+
63+
ExplainStatement.Option format = explain.getOption(ExplainStatement.OptionType.FORMAT);
64+
assertThat(format).isNotNull().extracting(ExplainStatement.Option::getValue).isEqualTo("JSON");
65+
66+
ExplainStatement.Option buffers = explain.getOption(ExplainStatement.OptionType.BUFFERS);
67+
assertThat(buffers).isNotNull().extracting(ExplainStatement.Option::getValue).isEqualTo("FALSE");
68+
}
2269
}

0 commit comments

Comments
 (0)