Skip to content

Commit 4c8f593

Browse files
authored
Support PostgreSQL parenthesized EXPLAIN options (#2558)
1 parent 7e15aae commit 4c8f593

4 files changed

Lines changed: 263 additions & 39 deletions

File tree

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

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
package net.sf.jsqlparser.statement;
1111

1212
import java.io.Serializable;
13+
import java.util.ArrayList;
1314
import java.util.LinkedHashMap;
1415
import java.util.List;
1516
import java.util.Locale;
16-
import java.util.stream.Collectors;
1717
import net.sf.jsqlparser.schema.Table;
1818

1919
/**
@@ -22,7 +22,8 @@
2222
public class ExplainStatement implements Statement {
2323
private String keyword;
2424
private Statement statement;
25-
private LinkedHashMap<OptionType, Option> options;
25+
private List<Option> options = new ArrayList<>();
26+
private boolean parenthesizedOptions;
2627
private Table table;
2728

2829
public ExplainStatement(String keyword) {
@@ -42,7 +43,7 @@ public ExplainStatement(String keyword, Statement statement, List<Option> option
4243
this.keyword = keyword;
4344
setStatement(statement);
4445

45-
initializeOptions(optionList);
46+
setOptionList(optionList);
4647
}
4748

4849
public ExplainStatement(Statement statement) {
@@ -72,29 +73,59 @@ public ExplainStatement setStatement(Statement statement) {
7273
}
7374

7475
public LinkedHashMap<OptionType, Option> getOptions() {
75-
return options == null ? null : new LinkedHashMap<>(options);
76+
if (options.isEmpty()) {
77+
return null;
78+
}
79+
LinkedHashMap<OptionType, Option> result = new LinkedHashMap<>();
80+
for (Option option : options) {
81+
result.put(option.getType(), option);
82+
}
83+
return result;
84+
}
85+
86+
/** Ordered options, including repetitions; the returned list is a defensive copy. */
87+
public List<Option> getOptionList() {
88+
return new ArrayList<>(options);
7689
}
7790

91+
public void setOptionList(List<Option> optionList) {
92+
options = optionList == null ? new ArrayList<>() : new ArrayList<>(optionList);
93+
}
94+
95+
public boolean isParenthesizedOptions() {
96+
return parenthesizedOptions;
97+
}
98+
99+
public ExplainStatement setParenthesizedOptions(boolean parenthesizedOptions) {
100+
this.parenthesizedOptions = parenthesizedOptions;
101+
return this;
102+
}
103+
104+
/** Adds an option, or replaces the last existing option of the same type. */
78105
public void addOption(Option option) {
79-
if (options == null) {
80-
options = new LinkedHashMap<>();
106+
for (int i = options.size() - 1; i >= 0; i--) {
107+
if (options.get(i).getType() == option.getType()) {
108+
options.set(i, option);
109+
return;
110+
}
81111
}
82-
83-
options.put(option.getType(), option);
112+
options.add(option);
84113
}
85114

86115
/**
87-
* Returns the first option that matches this optionType
116+
* Returns the last option that matches this optionType.
88117
*
89118
* @param optionType the option type to retrieve an Option for
90-
* @return an option of that type, or null. In case of duplicate options, the first found option
119+
* @return an option of that type, or null. In case of duplicate options, the last found option
91120
* will be returned.
92121
*/
93122
public Option getOption(OptionType optionType) {
94-
if (options == null) {
95-
return null;
123+
for (int i = options.size() - 1; i >= 0; i--) {
124+
if (options.get(i).getType() == optionType) {
125+
return options.get(i);
126+
}
96127
}
97-
return options.get(optionType);
128+
return null;
98129
}
99130

100131
public String getKeyword() {
@@ -112,12 +143,7 @@ public String toString() {
112143
if (table != null) {
113144
builder.append(" ").append(table);
114145
} else {
115-
if (options != null) {
116-
builder.append(" ");
117-
builder.append(options.values().stream().map(Option::formatOption)
118-
.collect(Collectors.joining(" ")));
119-
}
120-
146+
appendOptionsTo(builder);
121147
builder.append(" ");
122148
if (statement != null) {
123149
builder.append(statement);
@@ -132,17 +158,25 @@ public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
132158
return statementVisitor.visit(this, context);
133159
}
134160

135-
private void initializeOptions(List<Option> optionList) {
136-
if (optionList != null && !optionList.isEmpty()) {
137-
options = new LinkedHashMap<>();
138-
for (Option o : optionList) {
139-
options.put(o.getType(), o);
161+
/** Shared by SQL rendering and statement deparsers; includes the leading separator. */
162+
public StringBuilder appendOptionsTo(StringBuilder builder) {
163+
if (!options.isEmpty()) {
164+
builder.append(parenthesizedOptions ? " (" : " ");
165+
for (int i = 0; i < options.size(); i++) {
166+
if (i > 0) {
167+
builder.append(parenthesizedOptions ? ", " : " ");
168+
}
169+
builder.append(options.get(i).formatOption());
170+
}
171+
if (parenthesizedOptions) {
172+
builder.append(")");
140173
}
141174
}
175+
return builder;
142176
}
143177

144178
public enum OptionType {
145-
ANALYZE, VERBOSE, COSTS, BUFFERS, FORMAT, PLAN, PLAN_FOR;
179+
ANALYZE, VERBOSE, COSTS, BUFFERS, FORMAT, PLAN, PLAN_FOR, TIMING, SUMMARY, SETTINGS, WAL, GENERIC_PLAN, SERIALIZE, MEMORY;
146180

147181
public static OptionType from(String type) {
148182
return Enum.valueOf(OptionType.class, type.toUpperCase(Locale.ROOT));
@@ -171,7 +205,7 @@ public void setValue(String value) {
171205
}
172206

173207
public String formatOption() {
174-
return type.name().replace("_", " ") + (value != null
208+
return (type == OptionType.PLAN_FOR ? "PLAN FOR" : type.name()) + (value != null
175209
? " " + value
176210
: "");
177211
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,11 @@ public <S> StringBuilder visit(DescribeStatement describe, S context) {
415415

416416
@Override
417417
public <S> StringBuilder visit(ExplainStatement explainStatement, S context) {
418-
builder.append(explainStatement.getKeyword()).append(" ");
418+
builder.append(explainStatement.getKeyword());
419419
if (explainStatement.getTable() != null) {
420-
builder.append(explainStatement.getTable());
421-
} else if (explainStatement.getOptions() != null) {
422-
builder.append(explainStatement.getOptions().values().stream()
423-
.map(ExplainStatement.Option::formatOption).collect(Collectors.joining(" ")));
424-
builder.append(" ");
420+
builder.append(" ").append(explainStatement.getTable());
421+
} else {
422+
explainStatement.appendOptionsTo(builder).append(" ");
425423
}
426424
if (explainStatement.getStatement() != null) {
427425
explainStatement.getStatement().accept(this, context);

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

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,51 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
13171317
return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect);
13181318
}
13191319

1320+
private ExplainStatement.OptionType postgresqlExplainOptionType(String name) throws ParseException {
1321+
try {
1322+
return ExplainStatement.OptionType.from(name);
1323+
} catch (IllegalArgumentException ex) {
1324+
throw new ParseException("Unknown PostgreSQL EXPLAIN option: " + name);
1325+
}
1326+
}
1327+
1328+
private void validatePostgresqlExplain(ExplainStatement explain) throws ParseException {
1329+
if (explain.getTable() != null) {
1330+
throw new ParseException("PostgreSQL EXPLAIN requires a statement");
1331+
}
1332+
int previous = -1;
1333+
for (ExplainStatement.Option option : explain.getOptionList()) {
1334+
ExplainStatement.OptionType type = option.getType();
1335+
String value = option.getValue();
1336+
if (!explain.isParenthesizedOptions()) {
1337+
int position = type == ExplainStatement.OptionType.ANALYZE ? 0
1338+
: type == ExplainStatement.OptionType.VERBOSE ? 1 : -1;
1339+
if (position <= previous || value != null) {
1340+
throw new ParseException("PostgreSQL EXPLAIN options require parentheses");
1341+
}
1342+
previous = position;
1343+
} else {
1344+
boolean valid;
1345+
switch (type) {
1346+
case FORMAT:
1347+
valid = value != null && value.matches("(?i)TEXT|XML|JSON|YAML");
1348+
break;
1349+
case SERIALIZE:
1350+
valid = value == null || value.matches("(?i)NONE|TEXT|BINARY");
1351+
break;
1352+
case PLAN: case PLAN_FOR:
1353+
valid = false;
1354+
break;
1355+
default:
1356+
valid = value == null || value.matches("(?i)TRUE|FALSE|ON|OFF|0|1");
1357+
}
1358+
if (!valid) {
1359+
throw new ParseException("Invalid PostgreSQL EXPLAIN option: " + option.formatOption());
1360+
}
1361+
}
1362+
}
1363+
}
1364+
13201365
private void requireDdlSyntax(boolean valid, String message) throws ParseException {
13211366
if (!valid) {
13221367
throw new ParseException(message);
@@ -3884,12 +3929,16 @@ ExplainStatement Explain():
38843929
Table table;
38853930
List<ExplainStatement.Option> options;
38863931
ExplainStatement es;
3932+
boolean parenthesized = false;
38873933
}
38883934
{
38893935
( tk=<K_EXPLAIN> | tk = <K_SUMMARIZE> )
38903936
(
38913937
LOOKAHEAD(3)(
3892-
options= ExplainStatementOptions()
3938+
(
3939+
LOOKAHEAD(2) "(" options = PostgresqlExplainOptions() ")" { parenthesized = true; }
3940+
| options = ExplainStatementOptions()
3941+
)
38933942
(
38943943
[ LOOKAHEAD(2) with=WithList() ]
38953944
(
@@ -3902,6 +3951,7 @@ ExplainStatement Explain():
39023951
)
39033952
{
39043953
es = new ExplainStatement(tk.image, statement, options);
3954+
es.setParenthesizedOptions(parenthesized);
39053955
}
39063956
)
39073957
|
@@ -3910,35 +3960,65 @@ ExplainStatement Explain():
39103960
)
39113961
)
39123962
{
3963+
if (parenthesized || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) {
3964+
validatePostgresqlExplain(es);
3965+
}
39133966
return es;
39143967
}
39153968
}
39163969

3970+
List<ExplainStatement.Option> PostgresqlExplainOptions():
3971+
{
3972+
List<ExplainStatement.Option> options = new ArrayList<ExplainStatement.Option>();
3973+
ExplainStatement.Option option;
3974+
}
3975+
{
3976+
option = PostgresqlExplainOption() { options.add(option); }
3977+
( "," option = PostgresqlExplainOption() { options.add(option); } )*
3978+
{ return options; }
3979+
}
3980+
3981+
ExplainStatement.Option PostgresqlExplainOption():
3982+
{
3983+
Token name;
3984+
Token value = null;
3985+
}
3986+
{
3987+
( name = <K_ANALYZE> | name = <K_VERBOSE> | name = <K_COSTS>
3988+
| name = <K_BUFFERS> | name = <K_FORMAT> | name = <K_SETTINGS>
3989+
| name = <S_IDENTIFIER> )
3990+
[ ( value = <K_TRUE> | value = <K_FALSE> | value = <K_ON> | value = <K_OFF>
3991+
| value = <K_XML> | value = <K_JSON> | value = <K_YAML> | value = <K_TEXT_LITERAL>
3992+
| value = <K_NONE> | value = <K_BINARY> | value = <S_LONG> | value = <S_IDENTIFIER> ) ]
3993+
{
3994+
return new ExplainStatement.Option(postgresqlExplainOptionType(name.image))
3995+
.withValue(value == null ? null : value.image);
3996+
}
3997+
}
3998+
39173999
/**
3918-
* Postgres supports TRUE,ON,1,FALSE,OFF,0 as values
4000+
* Boolean values in the legacy unparenthesized syntax.
39194001
*/
39204002
String ExplainOptionBoolean():
39214003
{
39224004
Token tk = null;
39234005
}
39244006
{
3925-
// intentionally not supporting 0,1 at the moment
3926-
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )] // optional
4007+
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )]
39274008
{
39284009
return tk != null ? tk.image : null;
39294010
}
39304011
}
39314012

39324013
/**
3933-
* The output format, which can be TEXT, XML, JSON, or YAML
4014+
* Preserve the optional format in the legacy unparenthesized syntax.
39344015
*/
39354016
String ExplainFormatOption():
39364017
{
39374018
Token tk = null;
39384019
}
39394020
{
3940-
// TODO support Text
3941-
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )] // optional
4021+
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )]
39424022
{
39434023
return tk != null ? tk.image : null;
39444024
}

0 commit comments

Comments
 (0)