Skip to content

Commit cf89564

Browse files
committed
Support PostgreSQL parenthesized EXPLAIN options
1 parent 6c726d8 commit cf89564

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
@@ -408,13 +408,11 @@ public <S> StringBuilder visit(DescribeStatement describe, S context) {
408408

409409
@Override
410410
public <S> StringBuilder visit(ExplainStatement explainStatement, S context) {
411-
builder.append(explainStatement.getKeyword()).append(" ");
411+
builder.append(explainStatement.getKeyword());
412412
if (explainStatement.getTable() != null) {
413-
builder.append(explainStatement.getTable());
414-
} else if (explainStatement.getOptions() != null) {
415-
builder.append(explainStatement.getOptions().values().stream()
416-
.map(ExplainStatement.Option::formatOption).collect(Collectors.joining(" ")));
417-
builder.append(" ");
413+
builder.append(" ").append(explainStatement.getTable());
414+
} else {
415+
explainStatement.appendOptionsTo(builder).append(" ");
418416
}
419417
if (explainStatement.getStatement() != null) {
420418
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
@@ -1305,6 +1305,51 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
13051305
return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect);
13061306
}
13071307

1308+
private ExplainStatement.OptionType postgresqlExplainOptionType(String name) throws ParseException {
1309+
try {
1310+
return ExplainStatement.OptionType.from(name);
1311+
} catch (IllegalArgumentException ex) {
1312+
throw new ParseException("Unknown PostgreSQL EXPLAIN option: " + name);
1313+
}
1314+
}
1315+
1316+
private void validatePostgresqlExplain(ExplainStatement explain) throws ParseException {
1317+
if (explain.getTable() != null) {
1318+
throw new ParseException("PostgreSQL EXPLAIN requires a statement");
1319+
}
1320+
int previous = -1;
1321+
for (ExplainStatement.Option option : explain.getOptionList()) {
1322+
ExplainStatement.OptionType type = option.getType();
1323+
String value = option.getValue();
1324+
if (!explain.isParenthesizedOptions()) {
1325+
int position = type == ExplainStatement.OptionType.ANALYZE ? 0
1326+
: type == ExplainStatement.OptionType.VERBOSE ? 1 : -1;
1327+
if (position <= previous || value != null) {
1328+
throw new ParseException("PostgreSQL EXPLAIN options require parentheses");
1329+
}
1330+
previous = position;
1331+
} else {
1332+
boolean valid;
1333+
switch (type) {
1334+
case FORMAT:
1335+
valid = value != null && value.matches("(?i)TEXT|XML|JSON|YAML");
1336+
break;
1337+
case SERIALIZE:
1338+
valid = value == null || value.matches("(?i)NONE|TEXT|BINARY");
1339+
break;
1340+
case PLAN: case PLAN_FOR:
1341+
valid = false;
1342+
break;
1343+
default:
1344+
valid = value == null || value.matches("(?i)TRUE|FALSE|ON|OFF|0|1");
1345+
}
1346+
if (!valid) {
1347+
throw new ParseException("Invalid PostgreSQL EXPLAIN option: " + option.formatOption());
1348+
}
1349+
}
1350+
}
1351+
}
1352+
13081353
private void requireDdlSyntax(boolean valid, String message) throws ParseException {
13091354
if (!valid) {
13101355
throw new ParseException(message);
@@ -3872,12 +3917,16 @@ ExplainStatement Explain():
38723917
Table table;
38733918
List<ExplainStatement.Option> options;
38743919
ExplainStatement es;
3920+
boolean parenthesized = false;
38753921
}
38763922
{
38773923
( tk=<K_EXPLAIN> | tk = <K_SUMMARIZE> )
38783924
(
38793925
LOOKAHEAD(3)(
3880-
options= ExplainStatementOptions()
3926+
(
3927+
LOOKAHEAD(2) "(" options = PostgresqlExplainOptions() ")" { parenthesized = true; }
3928+
| options = ExplainStatementOptions()
3929+
)
38813930
(
38823931
[ LOOKAHEAD(2) with=WithList() ]
38833932
(
@@ -3890,6 +3939,7 @@ ExplainStatement Explain():
38903939
)
38913940
{
38923941
es = new ExplainStatement(tk.image, statement, options);
3942+
es.setParenthesizedOptions(parenthesized);
38933943
}
38943944
)
38953945
|
@@ -3898,35 +3948,65 @@ ExplainStatement Explain():
38983948
)
38993949
)
39003950
{
3951+
if (parenthesized || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) {
3952+
validatePostgresqlExplain(es);
3953+
}
39013954
return es;
39023955
}
39033956
}
39043957

3958+
List<ExplainStatement.Option> PostgresqlExplainOptions():
3959+
{
3960+
List<ExplainStatement.Option> options = new ArrayList<ExplainStatement.Option>();
3961+
ExplainStatement.Option option;
3962+
}
3963+
{
3964+
option = PostgresqlExplainOption() { options.add(option); }
3965+
( "," option = PostgresqlExplainOption() { options.add(option); } )*
3966+
{ return options; }
3967+
}
3968+
3969+
ExplainStatement.Option PostgresqlExplainOption():
3970+
{
3971+
Token name;
3972+
Token value = null;
3973+
}
3974+
{
3975+
( name = <K_ANALYZE> | name = <K_VERBOSE> | name = <K_COSTS>
3976+
| name = <K_BUFFERS> | name = <K_FORMAT> | name = <K_SETTINGS>
3977+
| name = <S_IDENTIFIER> )
3978+
[ ( value = <K_TRUE> | value = <K_FALSE> | value = <K_ON> | value = <K_OFF>
3979+
| value = <K_XML> | value = <K_JSON> | value = <K_YAML> | value = <K_TEXT_LITERAL>
3980+
| value = <K_NONE> | value = <K_BINARY> | value = <S_LONG> | value = <S_IDENTIFIER> ) ]
3981+
{
3982+
return new ExplainStatement.Option(postgresqlExplainOptionType(name.image))
3983+
.withValue(value == null ? null : value.image);
3984+
}
3985+
}
3986+
39053987
/**
3906-
* Postgres supports TRUE,ON,1,FALSE,OFF,0 as values
3988+
* Boolean values in the legacy unparenthesized syntax.
39073989
*/
39083990
String ExplainOptionBoolean():
39093991
{
39103992
Token tk = null;
39113993
}
39123994
{
3913-
// intentionally not supporting 0,1 at the moment
3914-
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )] // optional
3995+
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )]
39153996
{
39163997
return tk != null ? tk.image : null;
39173998
}
39183999
}
39194000

39204001
/**
3921-
* The output format, which can be TEXT, XML, JSON, or YAML
4002+
* Preserve the optional format in the legacy unparenthesized syntax.
39224003
*/
39234004
String ExplainFormatOption():
39244005
{
39254006
Token tk = null;
39264007
}
39274008
{
3928-
// TODO support Text
3929-
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )] // optional
4009+
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )]
39304010
{
39314011
return tk != null ? tk.image : null;
39324012
}

0 commit comments

Comments
 (0)