Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 60 additions & 26 deletions src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
package net.sf.jsqlparser.statement;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import net.sf.jsqlparser.schema.Table;

/**
Expand All @@ -22,7 +22,8 @@
public class ExplainStatement implements Statement {
private String keyword;
private Statement statement;
private LinkedHashMap<OptionType, Option> options;
private List<Option> options = new ArrayList<>();
private boolean parenthesizedOptions;
private Table table;

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

initializeOptions(optionList);
setOptionList(optionList);
}

public ExplainStatement(Statement statement) {
Expand Down Expand Up @@ -72,29 +73,59 @@ public ExplainStatement setStatement(Statement statement) {
}

public LinkedHashMap<OptionType, Option> getOptions() {
return options == null ? null : new LinkedHashMap<>(options);
if (options.isEmpty()) {
return null;
}
LinkedHashMap<OptionType, Option> result = new LinkedHashMap<>();
for (Option option : options) {
result.put(option.getType(), option);
}
return result;
}

/** Ordered options, including repetitions; the returned list is a defensive copy. */
public List<Option> getOptionList() {
return new ArrayList<>(options);
}

public void setOptionList(List<Option> optionList) {
options = optionList == null ? new ArrayList<>() : new ArrayList<>(optionList);
}

public boolean isParenthesizedOptions() {
return parenthesizedOptions;
}

public ExplainStatement setParenthesizedOptions(boolean parenthesizedOptions) {
this.parenthesizedOptions = parenthesizedOptions;
return this;
}

/** Adds an option, or replaces the last existing option of the same type. */
public void addOption(Option option) {
if (options == null) {
options = new LinkedHashMap<>();
for (int i = options.size() - 1; i >= 0; i--) {
if (options.get(i).getType() == option.getType()) {
options.set(i, option);
return;
}
}

options.put(option.getType(), option);
options.add(option);
}

/**
* Returns the first option that matches this optionType
* Returns the last option that matches this optionType.
*
* @param optionType the option type to retrieve an Option for
* @return an option of that type, or null. In case of duplicate options, the first found option
* @return an option of that type, or null. In case of duplicate options, the last found option
* will be returned.
*/
public Option getOption(OptionType optionType) {
if (options == null) {
return null;
for (int i = options.size() - 1; i >= 0; i--) {
if (options.get(i).getType() == optionType) {
return options.get(i);
}
}
return options.get(optionType);
return null;
}

public String getKeyword() {
Expand All @@ -112,12 +143,7 @@ public String toString() {
if (table != null) {
builder.append(" ").append(table);
} else {
if (options != null) {
builder.append(" ");
builder.append(options.values().stream().map(Option::formatOption)
.collect(Collectors.joining(" ")));
}

appendOptionsTo(builder);
builder.append(" ");
if (statement != null) {
builder.append(statement);
Expand All @@ -132,17 +158,25 @@ public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
return statementVisitor.visit(this, context);
}

private void initializeOptions(List<Option> optionList) {
if (optionList != null && !optionList.isEmpty()) {
options = new LinkedHashMap<>();
for (Option o : optionList) {
options.put(o.getType(), o);
/** Shared by SQL rendering and statement deparsers; includes the leading separator. */
public StringBuilder appendOptionsTo(StringBuilder builder) {
if (!options.isEmpty()) {
builder.append(parenthesizedOptions ? " (" : " ");
for (int i = 0; i < options.size(); i++) {
if (i > 0) {
builder.append(parenthesizedOptions ? ", " : " ");
}
builder.append(options.get(i).formatOption());
}
if (parenthesizedOptions) {
builder.append(")");
}
}
return builder;
}

public enum OptionType {
ANALYZE, VERBOSE, COSTS, BUFFERS, FORMAT, PLAN, PLAN_FOR;
ANALYZE, VERBOSE, COSTS, BUFFERS, FORMAT, PLAN, PLAN_FOR, TIMING, SUMMARY, SETTINGS, WAL, GENERIC_PLAN, SERIALIZE, MEMORY;

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

public String formatOption() {
return type.name().replace("_", " ") + (value != null
return (type == OptionType.PLAN_FOR ? "PLAN FOR" : type.name()) + (value != null
? " " + value
: "");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,11 @@ public <S> StringBuilder visit(DescribeStatement describe, S context) {

@Override
public <S> StringBuilder visit(ExplainStatement explainStatement, S context) {
builder.append(explainStatement.getKeyword()).append(" ");
builder.append(explainStatement.getKeyword());
if (explainStatement.getTable() != null) {
builder.append(explainStatement.getTable());
} else if (explainStatement.getOptions() != null) {
builder.append(explainStatement.getOptions().values().stream()
.map(ExplainStatement.Option::formatOption).collect(Collectors.joining(" ")));
builder.append(" ");
builder.append(" ").append(explainStatement.getTable());
} else {
explainStatement.appendOptionsTo(builder).append(" ");
}
if (explainStatement.getStatement() != null) {
explainStatement.getStatement().accept(this, context);
Expand Down
94 changes: 87 additions & 7 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,51 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect);
}

private ExplainStatement.OptionType postgresqlExplainOptionType(String name) throws ParseException {
try {
return ExplainStatement.OptionType.from(name);
} catch (IllegalArgumentException ex) {
throw new ParseException("Unknown PostgreSQL EXPLAIN option: " + name);
}
}

private void validatePostgresqlExplain(ExplainStatement explain) throws ParseException {
if (explain.getTable() != null) {
throw new ParseException("PostgreSQL EXPLAIN requires a statement");
}
int previous = -1;
for (ExplainStatement.Option option : explain.getOptionList()) {
ExplainStatement.OptionType type = option.getType();
String value = option.getValue();
if (!explain.isParenthesizedOptions()) {
int position = type == ExplainStatement.OptionType.ANALYZE ? 0
: type == ExplainStatement.OptionType.VERBOSE ? 1 : -1;
if (position <= previous || value != null) {
throw new ParseException("PostgreSQL EXPLAIN options require parentheses");
}
previous = position;
} else {
boolean valid;
switch (type) {
case FORMAT:
valid = value != null && value.matches("(?i)TEXT|XML|JSON|YAML");
break;
case SERIALIZE:
valid = value == null || value.matches("(?i)NONE|TEXT|BINARY");
break;
case PLAN: case PLAN_FOR:
valid = false;
break;
default:
valid = value == null || value.matches("(?i)TRUE|FALSE|ON|OFF|0|1");
}
if (!valid) {
throw new ParseException("Invalid PostgreSQL EXPLAIN option: " + option.formatOption());
}
}
}
}

private void requireDdlSyntax(boolean valid, String message) throws ParseException {
if (!valid) {
throw new ParseException(message);
Expand Down Expand Up @@ -3872,12 +3917,16 @@ ExplainStatement Explain():
Table table;
List<ExplainStatement.Option> options;
ExplainStatement es;
boolean parenthesized = false;
}
{
( tk=<K_EXPLAIN> | tk = <K_SUMMARIZE> )
(
LOOKAHEAD(3)(
options= ExplainStatementOptions()
(
LOOKAHEAD(2) "(" options = PostgresqlExplainOptions() ")" { parenthesized = true; }
| options = ExplainStatementOptions()
)
(
[ LOOKAHEAD(2) with=WithList() ]
(
Expand All @@ -3890,6 +3939,7 @@ ExplainStatement Explain():
)
{
es = new ExplainStatement(tk.image, statement, options);
es.setParenthesizedOptions(parenthesized);
}
)
|
Expand All @@ -3898,35 +3948,65 @@ ExplainStatement Explain():
)
)
{
if (parenthesized || Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) {
validatePostgresqlExplain(es);
}
return es;
}
}

List<ExplainStatement.Option> PostgresqlExplainOptions():
{
List<ExplainStatement.Option> options = new ArrayList<ExplainStatement.Option>();
ExplainStatement.Option option;
}
{
option = PostgresqlExplainOption() { options.add(option); }
( "," option = PostgresqlExplainOption() { options.add(option); } )*
{ return options; }
}

ExplainStatement.Option PostgresqlExplainOption():
{
Token name;
Token value = null;
}
{
( name = <K_ANALYZE> | name = <K_VERBOSE> | name = <K_COSTS>
| name = <K_BUFFERS> | name = <K_FORMAT> | name = <K_SETTINGS>
| name = <S_IDENTIFIER> )
[ ( value = <K_TRUE> | value = <K_FALSE> | value = <K_ON> | value = <K_OFF>
| value = <K_XML> | value = <K_JSON> | value = <K_YAML> | value = <K_TEXT_LITERAL>
| value = <K_NONE> | value = <K_BINARY> | value = <S_LONG> | value = <S_IDENTIFIER> ) ]
{
return new ExplainStatement.Option(postgresqlExplainOptionType(name.image))
.withValue(value == null ? null : value.image);
}
}

/**
* Postgres supports TRUE,ON,1,FALSE,OFF,0 as values
* Boolean values in the legacy unparenthesized syntax.
*/
String ExplainOptionBoolean():
{
Token tk = null;
}
{
// intentionally not supporting 0,1 at the moment
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )] // optional
[( tk=<K_TRUE> | tk=<K_FALSE> | tk=<K_ON> | tk=<K_OFF> )]
{
return tk != null ? tk.image : null;
}
}

/**
* The output format, which can be TEXT, XML, JSON, or YAML
* Preserve the optional format in the legacy unparenthesized syntax.
*/
String ExplainFormatOption():
{
Token tk = null;
}
{
// TODO support Text
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )] // optional
[( tk=<K_XML> | tk=<K_JSON> | tk=<K_YAML> )]
{
return tk != null ? tk.image : null;
}
Expand Down
Loading
Loading