From ff52fdbce09171a9af4bf60637cb4c1b107cccfd Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 14 Oct 2026 12:56:13 +0900 Subject: [PATCH] feat: share typed MySQL index options across CREATE INDEX and table definitions Parse MySQL index options with one IndexSpecOption production in CREATE INDEX, CREATE TABLE key definitions and ALTER TABLE ADD/ALTER INDEX, and keep them as ordered typed IndexSpecOption nodes on IndexOptions. The ALGORITHM and LOCK clauses of CREATE INDEX and DROP INDEX become AlgorithmOrLockOption nodes. PRIMARY KEY and named UNIQUE keys accept the index type before the key list. getIndexSpec(), CreateIndex.getTailParameters() and Drop.getParameters() remain available as derived token snapshots. Signed-off-by: minleejae --- .../create/index/AlgorithmOrLockOption.java | 85 ++++ .../statement/create/index/CreateIndex.java | 63 ++- .../statement/create/table/Index.java | 5 + .../create/table/IndexOptionSource.java | 8 + .../statement/create/table/IndexOptions.java | 57 ++- .../create/table/IndexSpecOption.java | 200 +++++++++ .../statement/create/table/KeyConstraint.java | 5 + .../sf/jsqlparser/statement/drop/Drop.java | 85 +++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 209 +++++---- src/site/sphinx/usage.rst | 79 +++- .../jsqlparser/statement/alter/AlterTest.java | 31 +- .../create/MySqlIndexSpecOptionTest.java | 421 ++++++++++++++++++ 12 files changed, 1105 insertions(+), 143 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/index/AlgorithmOrLockOption.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/AlgorithmOrLockOption.java b/src/main/java/net/sf/jsqlparser/statement/create/index/AlgorithmOrLockOption.java new file mode 100644 index 000000000..64bc22aef --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/AlgorithmOrLockOption.java @@ -0,0 +1,85 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.index; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * MySQL's {@code ALGORITHM [=] value} or {@code LOCK [=] value} clause of CREATE INDEX and DROP + * INDEX. The value keeps its SQL spelling, such as INPLACE, COPY, NONE or SHARED. + */ +public class AlgorithmOrLockOption implements Serializable { + + public enum Kind { + ALGORITHM, LOCK + } + + private Kind kind; + private String value; + private boolean useEquals; + + public AlgorithmOrLockOption(Kind kind, String value, boolean useEquals) { + this.kind = Objects.requireNonNull(kind, "kind"); + this.value = Objects.requireNonNull(value, "value"); + this.useEquals = useEquals; + } + + public static AlgorithmOrLockOption algorithm(String value) { + return new AlgorithmOrLockOption(Kind.ALGORITHM, value, true); + } + + public static AlgorithmOrLockOption lock(String value) { + return new AlgorithmOrLockOption(Kind.LOCK, value, true); + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = Objects.requireNonNull(kind, "kind"); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = Objects.requireNonNull(value, "value"); + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + /** Returns an unmodifiable token snapshot, such as {@code [ALGORITHM, =, INPLACE]}. */ + public List getTokens() { + List tokens = new ArrayList<>(); + tokens.add(kind.name()); + if (useEquals) { + tokens.add("="); + } + tokens.add(value); + return Collections.unmodifiableList(tokens); + } + + @Override + public String toString() { + return String.join(" ", getTokens()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java index b2d277611..9e5739ae5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/index/CreateIndex.java @@ -23,7 +23,7 @@ public class CreateIndex implements Statement { private Table table; private Index index; private IndexOptions detachedOptions; - private List tailParameters; + private final List algorithmOrLockOptions = new ArrayList<>(); private boolean indexTypeBeforeOn = false; private boolean usingIfNotExists = false; private boolean concurrently; @@ -170,6 +170,7 @@ public void setIndex(Index index) { detachedOptions.setNullsDistinct(previousOptions.getNullsDistinct()); detachedOptions.setStorageParameters(previousOptions.getStorageParameters()); detachedOptions.setTableSpace(previousOptions.getTableSpace()); + detachedOptions.setSpecOptions(previousOptions.getSpecOptions()); } } else { if (previousOptions != null && previousOptions != index.getIndexOptions()) { @@ -185,6 +186,9 @@ public void setIndex(Index index) { if (index.getTableSpace() == null) { index.setTableSpace(previousOptions.getTableSpace()); } + if (index.getSpecOptions() == null) { + index.setSpecOptions(previousOptions.getSpecOptions()); + } } detachedOptions = null; } @@ -199,12 +203,56 @@ public void setTable(Table table) { this.table = table; } + /** + * Returns the mutable MySQL ALGORITHM and LOCK clauses in source order. They are rendered after + * the index options, as MySQL requires. + */ + public List getAlgorithmOrLockOptions() { + return algorithmOrLockOptions; + } + + /** Copies the list membership and retains the option nodes; null clears the clauses. */ + public void setAlgorithmOrLockOptions(List options) { + algorithmOrLockOptions.clear(); + if (options != null) { + algorithmOrLockOptions.addAll(options); + } + } + + public CreateIndex withAlgorithmOrLockOptions(List options) { + setAlgorithmOrLockOptions(options); + return this; + } + + public CreateIndex addAlgorithmOrLockOptions(AlgorithmOrLockOption... options) { + Collections.addAll(algorithmOrLockOptions, options); + return this; + } + + /** + * Returns an unmodifiable token snapshot of the trailing clauses: the index options after the + * key list ({@link Index#getSpecOptions()}), followed by {@link #getAlgorithmOrLockOptions()}. + * Edit those nodes instead of the snapshot. + */ public List getTailParameters() { - return tailParameters; + List tokens = new ArrayList<>(); + IndexOptions options = getOptions(); + if (options != null && options.getIndexSpec() != null) { + tokens.addAll(options.getIndexSpec()); + } + for (AlgorithmOrLockOption option : algorithmOrLockOptions) { + tokens.addAll(option.getTokens()); + } + return Collections.unmodifiableList(tokens); } + /** + * Replaces the index options after the key list and the ALGORITHM and LOCK clauses with one + * opaque option holding a copy of the tokens, without parsing them. Null clears them. + */ public void setTailParameters(List tailParameters) { - this.tailParameters = tailParameters; + algorithmOrLockOptions.clear(); + getOrCreateOptions().setIndexSpec(tailParameters); } @Override @@ -223,11 +271,14 @@ public StringBuilder appendTo(StringBuilder buffer, Consumer express appendIndexTarget(buffer); appendIndexColumns(buffer, expressionPrinter); appendPostgreSqlTail(buffer, expressionPrinter); - if (tailParameters != null) { - for (String param : tailParameters) { - buffer.append(" ").append(param); + if (index.getSpecOptions() != null) { + for (IndexSpecOption option : index.getSpecOptions()) { + buffer.append(" ").append(option); } } + for (AlgorithmOrLockOption option : algorithmOrLockOptions) { + buffer.append(" ").append(option); + } return buffer; } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index f0b1a1dea..9c809447f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -183,6 +183,11 @@ public Index withIndexSpec(List value) { return this; } + public Index withSpecOptions(List value) { + setSpecOptions(value); + return this; + } + public Index withClustering(IndexOptions.Clustering value) { setClustering(value); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java index 99037db13..4c016d32a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptionSource.java @@ -33,6 +33,14 @@ default void setIndexSpec(List value) { getIndexOptions().setIndexSpec(value); } + default List getSpecOptions() { + return getIndexOptions().getSpecOptions(); + } + + default void setSpecOptions(List value) { + getIndexOptions().setSpecOptions(value); + } + default String getCommentText() { return getIndexOptions().getCommentText(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java index 2e6cc0788..f6050b4bc 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexOptions.java @@ -11,6 +11,9 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; @@ -23,7 +26,7 @@ public enum Clustering { } private String using; - private List indexSpec; + private List specOptions; private String commentText; private Clustering clustering; private Boolean nullsDistinct; @@ -39,12 +42,60 @@ public void setUsing(String using) { this.using = using; } + /** + * Returns the mutable, ordered options written after the key list, or null when unset. An index + * type written before the key list is {@link #getUsing()} instead. + */ + public List getSpecOptions() { + return specOptions; + } + + /** Copies the list membership and retains the option nodes; null clears the options. */ + public void setSpecOptions(List specOptions) { + this.specOptions = specOptions == null ? null : new ArrayList<>(specOptions); + } + + public IndexOptions addSpecOptions(IndexSpecOption... options) { + return addSpecOptions(Arrays.asList(options)); + } + + public IndexOptions addSpecOptions(Collection options) { + if (specOptions == null) { + specOptions = new ArrayList<>(); + } + specOptions.addAll(options); + return this; + } + + /** + * Returns an unmodifiable token snapshot of {@link #getSpecOptions()}, such as + * {@code [KEY_BLOCK_SIZE, =, 8, COMMENT, 'text']}, or null when unset. Edit the option nodes + * instead of the snapshot. + */ public List getIndexSpec() { - return indexSpec; + if (specOptions == null) { + return null; + } + List tokens = new ArrayList<>(); + for (IndexSpecOption option : specOptions) { + tokens.addAll(option.getTokens()); + } + return Collections.unmodifiableList(tokens); } + /** + * Replaces all options after the key list with one opaque option holding a copy of the tokens, + * without parsing them. Null clears the options; an empty list stores no options. + */ public void setIndexSpec(List indexSpec) { - this.indexSpec = indexSpec; + if (indexSpec == null) { + specOptions = null; + return; + } + specOptions = new ArrayList<>(); + if (!indexSpec.isEmpty()) { + specOptions.add(IndexSpecOption.raw(indexSpec)); + } } public String getCommentText() { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java new file mode 100644 index 000000000..9ff6fa655 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/IndexSpecOption.java @@ -0,0 +1,200 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.table; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.expression.StringValue; + +/** + * An option written after the key list of an index or key definition. MySQL's index options + * (KEY_BLOCK_SIZE, USING index type, WITH PARSER, COMMENT, VISIBLE/INVISIBLE, ENGINE_ATTRIBUTE and + * SECONDARY_ENGINE_ATTRIBUTE) are typed; other accepted tokens are retained as opaque + * {@link Kind#OTHER} options. The kind is fixed; replace the node to change it. + */ +public class IndexSpecOption implements Serializable { + + public enum Kind { + KEY_BLOCK_SIZE, INDEX_TYPE, PARSER, COMMENT, VISIBILITY, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER + } + + private final Kind kind; + private String value; + private StringValue comment; + private boolean useEquals; + private boolean visible; + private List tokens; + + private IndexSpecOption(Kind kind) { + this.kind = kind; + } + + /** + * Creates a valued option. {@code value} keeps its SQL spelling: a number for KEY_BLOCK_SIZE, + * an index type or parser name, or a quoted string literal for COMMENT and the engine + * attributes. + */ + public static IndexSpecOption of(Kind kind, String value) { + return of(kind, value, false); + } + + /** Like {@link #of(Kind, String)}; {@code useEquals} requires a kind that accepts '='. */ + public static IndexSpecOption of(Kind kind, String value, boolean useEquals) { + IndexSpecOption option = new IndexSpecOption(Objects.requireNonNull(kind, "kind")); + option.setValue(value); + option.setUseEquals(useEquals); + return option; + } + + /** + * Creates a COMMENT option. The literal keeps its quotes and escaping, so + * {@link StringValue#getNotExcapedValue()} returns the stored comment text. + */ + public static IndexSpecOption comment(StringValue comment) { + IndexSpecOption option = new IndexSpecOption(Kind.COMMENT); + option.setComment(comment); + return option; + } + + public static IndexSpecOption visibility(boolean visible) { + IndexSpecOption option = new IndexSpecOption(Kind.VISIBILITY); + option.visible = visible; + return option; + } + + /** Creates an opaque option from a copy of {@code tokens}; the tokens are never parsed. */ + public static IndexSpecOption raw(List tokens) { + IndexSpecOption option = new IndexSpecOption(Kind.OTHER); + option.setTokens(tokens); + return option; + } + + public static IndexSpecOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public Kind getKind() { + return kind; + } + + /** + * The SQL spelling of the option value, or null for VISIBILITY and opaque options. For COMMENT, + * this is the literal's SQL text, such as {@code 'it\'s'}; use {@link #getComment()} for its + * decoded text. + */ + public String getValue() { + return kind == Kind.COMMENT ? comment.toString() : value; + } + + /** + * Replaces the value with its SQL spelling. A COMMENT value is read as a string literal and + * keeps the escaping of the literal it replaces. + */ + public void setValue(String value) { + if (kind == Kind.VISIBILITY || kind == Kind.OTHER) { + throw new IllegalStateException(kind + " options have no value"); + } + Objects.requireNonNull(value, "value"); + if (kind == Kind.COMMENT) { + setComment(new StringValue(value) + .withEscaping(comment == null ? null : comment.getEscaping())); + } else { + this.value = value; + } + } + + /** The literal of a COMMENT option, otherwise null. */ + public StringValue getComment() { + return comment; + } + + public void setComment(StringValue comment) { + if (kind != Kind.COMMENT) { + throw new IllegalStateException(kind + " options have no comment"); + } + this.comment = Objects.requireNonNull(comment, "comment"); + } + + public boolean isUseEquals() { + return useEquals; + } + + /** + * Selects the optional '=' of KEY_BLOCK_SIZE, ENGINE_ATTRIBUTE and SECONDARY_ENGINE_ATTRIBUTE. + */ + public void setUseEquals(boolean useEquals) { + if (useEquals && kind != Kind.KEY_BLOCK_SIZE && kind != Kind.ENGINE_ATTRIBUTE + && kind != Kind.SECONDARY_ENGINE_ATTRIBUTE) { + throw new IllegalStateException(kind + " options do not use '='"); + } + this.useEquals = useEquals; + } + + /** VISIBLE (true) or INVISIBLE (false) for a VISIBILITY option, otherwise null. */ + public Boolean getVisible() { + return kind == Kind.VISIBILITY ? visible : null; + } + + public void setVisible(boolean visible) { + if (kind != Kind.VISIBILITY) { + throw new IllegalStateException(kind + " options have no visibility"); + } + this.visible = visible; + } + + /** + * Returns an unmodifiable, detached token snapshot: the keywords, an optional '=' and the value + * of a typed option, or a copy of an opaque option's tokens. + */ + public List getTokens() { + if (kind == Kind.OTHER) { + return Collections.unmodifiableList(new ArrayList<>(tokens)); + } + List result = new ArrayList<>(Arrays.asList(keyword().split(" "))); + if (useEquals) { + result.add("="); + } + if (getValue() != null) { + result.add(getValue()); + } + return Collections.unmodifiableList(result); + } + + /** Replaces the tokens of an opaque option with a copy; typed options reject raw tokens. */ + public void setTokens(List tokens) { + if (kind != Kind.OTHER) { + throw new IllegalStateException( + "Typed " + kind + " options cannot be replaced with raw tokens"); + } + this.tokens = new ArrayList<>(Objects.requireNonNull(tokens, "tokens")); + } + + private String keyword() { + switch (kind) { + case INDEX_TYPE: + return "USING"; + case PARSER: + return "WITH PARSER"; + case VISIBILITY: + return visible ? "VISIBLE" : "INVISIBLE"; + default: + return kind.name(); + } + } + + @Override + public String toString() { + return String.join(" ", getTokens()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java index e48b01434..979fe4476 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/KeyConstraint.java @@ -113,6 +113,11 @@ public KeyConstraint withIndexSpec(List value) { return this; } + public KeyConstraint withSpecOptions(List value) { + setSpecOptions(value); + return this; + } + public KeyConstraint withClustering(IndexOptions.Clustering value) { setClustering(value); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java index d6dec72f0..57b0fdb4f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -24,6 +24,7 @@ import net.sf.jsqlparser.statement.RoutineReference; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.index.AlgorithmOrLockOption; import net.sf.jsqlparser.statement.create.user.UserAccount; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -56,6 +57,8 @@ public boolean hasOwnerTable() { private List parameters; private Table table; private int tablePosition; + private final List algorithmOrLockOptions = new ArrayList<>(); + private Integer algorithmOrLockPosition; private CascadeBehavior behavior; private Map> typeToParameters = new HashMap<>(); private boolean ifExists = false; @@ -242,19 +245,16 @@ private static Table qualifiedTable(String name) { } /** - * Returns the legacy tokens after the targets. With a structured ON table or CASCADE/RESTRICT - * behavior, this is a snapshot that includes them; edit {@link #setTable} or - * {@link #setBehavior} instead. + * Returns the legacy tokens after the targets. With a structured ON table, ALGORITHM or LOCK + * clauses or CASCADE/RESTRICT behavior, this is a snapshot that includes them in source order; + * edit {@link #setTable}, {@link #getAlgorithmOrLockOptions()} or {@link #setBehavior} instead. */ public List getParameters() { - if (table == null && behavior == null) { + if (table == null && algorithmOrLockOptions.isEmpty() && behavior == null) { return parameters; } - List tokens = parameters == null ? new ArrayList<>() : new ArrayList<>(parameters); - if (table != null) { - tokens.add(tablePosition, "ON"); - tokens.add(tablePosition + 1, table.toString()); - } + List tokens = new ArrayList<>(); + appendParameters(tokens::add, table -> tokens.add(table.toString())); if (behavior != null) { tokens.add(behavior.name()); } @@ -299,13 +299,15 @@ public void visitTables(Consumer visitor) { } /** - * Replaces all legacy parameters and clears the structured ON table and CASCADE/RESTRICT - * behavior. + * Replaces all legacy parameters and clears the structured ON table, the ALGORITHM and LOCK + * clauses and the CASCADE/RESTRICT behavior. */ public void setParameters(List list) { parameters = list; table = null; tablePosition = 0; + algorithmOrLockOptions.clear(); + algorithmOrLockPosition = null; behavior = null; } @@ -490,7 +492,10 @@ public StringBuilder appendTo(StringBuilder builder, Consumer
tablePrinte if (engine != null) { builder.append(engineUseEquals ? " ENGINE = " : " ENGINE ").append(engine); } - appendParameters(builder, tablePrinter); + appendParameters(token -> builder.append(' ').append(token), table -> { + builder.append(' '); + tablePrinter.accept(table); + }); if (behavior != null) { builder.append(' ').append(behavior); } @@ -526,15 +531,21 @@ private void appendNames(StringBuilder builder, Consumer
tablePrinter) { } } - private void appendParameters(StringBuilder builder, Consumer
tablePrinter) { + /** Emits the parameter tokens with ON and the ALGORITHM/LOCK clauses at their positions. */ + private void appendParameters(Consumer tokens, Consumer
onTable) { int size = parameters == null ? 0 : parameters.size(); + int optionPosition = algorithmOrLockPosition == null ? size + : Math.min(algorithmOrLockPosition, size); for (int i = 0; i <= size; i++) { if (table != null && i == tablePosition) { - builder.append(" ON "); - tablePrinter.accept(table); + tokens.accept("ON"); + onTable.accept(table); + } + if (i == optionPosition) { + algorithmOrLockOptions.forEach(option -> option.getTokens().forEach(tokens)); } if (i < size) { - builder.append(' ').append(parameters.get(i)); + tokens.accept(parameters.get(i)); } } } @@ -596,4 +607,46 @@ public Drop addParameters(Collection parameters) { this.parameters.addAll(parameters); return this; } + + /** + * Returns the mutable MySQL ALGORITHM and LOCK clauses of DROP INDEX in source order. They + * follow the parameter tokens unless positioned with + * {@link #setAlgorithmOrLockOptions(List, int)}. + */ + public List getAlgorithmOrLockOptions() { + return algorithmOrLockOptions; + } + + /** Copies the list membership and places the clauses after all parameter tokens. */ + public void setAlgorithmOrLockOptions(List options) { + setAlgorithmOrLockOptions(options, -1); + } + + /** + * Copies the list membership and places the clauses before the parameter token at + * {@code parameterPosition}; -1 places them after all parameter tokens. + */ + public void setAlgorithmOrLockOptions(List options, + int parameterPosition) { + int size = parameters == null ? 0 : parameters.size(); + if (parameterPosition < -1 || parameterPosition > size) { + throw new IllegalArgumentException( + "Invalid ALGORITHM/LOCK position: " + parameterPosition); + } + algorithmOrLockOptions.clear(); + if (options != null) { + algorithmOrLockOptions.addAll(options); + } + algorithmOrLockPosition = parameterPosition < 0 ? null : parameterPosition; + } + + public Drop withAlgorithmOrLockOptions(List options) { + setAlgorithmOrLockOptions(options); + return this; + } + + public Drop addAlgorithmOrLockOptions(AlgorithmOrLockOption... options) { + Collections.addAll(algorithmOrLockOptions, options); + return this; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f49b3ddba..6cd97c7d9 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2556,13 +2556,13 @@ public class CCJSqlParser extends AbstractJSqlParser { // Preserve generic ALTER's historical placement of USING and COMMENT options. if (!Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) && !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { - for (String option : new ArrayList(key.getIndexSpec())) { - if (option.toUpperCase(Locale.ROOT).startsWith("USING ")) { - alterExp.addParameters("USING", option.substring("USING ".length())); - key.getIndexSpec().remove(option); - } else if (option.toUpperCase(Locale.ROOT).startsWith("COMMENT ")) { - key.setCommentText(option.substring("COMMENT ".length())); - key.getIndexSpec().remove(option); + for (IndexSpecOption option : new ArrayList(key.getSpecOptions())) { + if (option.getKind() == IndexSpecOption.Kind.INDEX_TYPE) { + alterExp.addParameters("USING", option.getValue()); + key.getSpecOptions().remove(option); + } else if (option.getKind() == IndexSpecOption.Kind.COMMENT) { + key.setCommentText(option.getValue()); + key.getSpecOptions().remove(option); } } } @@ -15264,8 +15264,6 @@ CreateIndex CreateIndex(): Expression where = null; String indexType; Index index; - List parameter = new ArrayList(); - List tailParameters = new ArrayList(); ObjectNames name; } { @@ -15310,45 +15308,54 @@ CreateIndex CreateIndex(): { createIndex.setTableSpace(tableSpace); } ] [ LOOKAHEAD(2) where=Expression() { createIndex.setWhere(where); } ] ( LOOKAHEAD(2, { getToken(1).kind != K_LOCK || getToken(2).kind != K_TABLE }) - parameter=CreateIndexTailOption() { tailParameters.addAll(parameter); } )* + CreateIndexTailOption(createIndex, index) )* { index.setColumns(colNames); createIndex.setIndex(index); createIndex.setTable(table); - createIndex.setTailParameters(tailParameters); return createIndex; } } -List CreateIndexTailOption(): +/** + * Index options shared with table key definitions are kept on the index, ALGORITHM and LOCK on + * the statement. Other trailing tokens remain opaque index options. + */ +void CreateIndexTailOption(CreateIndex statement, Index index): { - List option; + AlgorithmOrLockOption algorithmOrLock; + IndexSpecOption option; + List parameter; } { ( LOOKAHEAD({ getToken(1).kind == K_ALGORITHM || getToken(1).kind == K_LOCK }) - option=IndexAlgorithmOrLockOption() + algorithmOrLock=IndexAlgorithmOrLockOption() + { statement.addAlgorithmOrLockOptions(algorithmOrLock); } | - option=CreateParameter() + LOOKAHEAD(IndexSpecOption(), { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) + option=IndexSpecOption() { index.getIndexOptions().addSpecOptions(option); } + | + parameter=CreateParameter() + { index.getIndexOptions().addSpecOptions(IndexSpecOption.raw(parameter)); } ) - { return option; } } // CREATE INDEX and DROP INDEX share MySQL's algorithm_option / lock_option syntax. -// Keep the legacy token list, including optional equals signs and the original order. -List IndexAlgorithmOrLockOption(): +AlgorithmOrLockOption IndexAlgorithmOrLockOption(): { - List option = new ArrayList(); - Token keyword; + AlgorithmOrLockOption.Kind kind; + boolean useEquals = false; Token value; } { - ( keyword= | keyword= ) { option.add(keyword.image); } - [ "=" { option.add("="); } ] + ( { kind = AlgorithmOrLockOption.Kind.ALGORITHM; } + | { kind = AlgorithmOrLockOption.Kind.LOCK; } ) + [ "=" { useEquals = true; } ] // COPY is one of MySQL's ALGORITHM values and also starts the COPY statement ( value= | value= | value= | value= | value= ) - { option.add(value.image); return option; } + { return new AlgorithmOrLockOption(kind, value.image, useEquals); } } List ColumnDefinitionParameter(): { @@ -16287,7 +16294,7 @@ TableElement TableIndexSpec(boolean createContext): String indexName = null; String using = null; List columns; - List indexOptions = new ArrayList(); + List indexOptions = new ArrayList(); TableElement index; Boolean nullsDistinct = null; IndexOptions.Clustering clustering = null; @@ -16296,15 +16303,19 @@ TableElement TableIndexSpec(boolean createContext): ( typeToken= keywordToken= clustering=SqlServerIndexClustering() - [ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ] + [ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + // MySQL's index_type may also precede the key list: PRIMARY KEY USING BTREE (id) + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX + && getToken(2).kind != K_COLUMNS }) using=UsingIndexType() ] columns=IndexKeyColumns(true) TableIndexOptions(createContext, indexOptions) { index = new KeyConstraint() .withIndexName(indexName) .withType(typeToken.image + " " + keywordToken.image) - .withColumns(columns) - .withIndexSpec(indexOptions); + .withUsing(using).withColumns(columns) + .withSpecOptions(indexOptions); } | typeToken= @@ -16326,12 +16337,12 @@ TableElement TableIndexSpec(boolean createContext): .withIndexName(indexName) .withType(typeToken.image + (keywordToken != null ? " " + keywordToken.image : "")) - .withUsing(using).withColumns(columns).withIndexSpec(indexOptions); + .withUsing(using).withColumns(columns).withSpecOptions(indexOptions); } else { index = new KeyConstraint().withType(typeToken.image) .withIndexKeyword(keywordToken != null ? keywordToken.image : null) .withIndexName(indexName).withUsing(using).withColumns(columns) - .withIndexSpec(indexOptions); + .withSpecOptions(indexOptions); } } | @@ -16345,7 +16356,7 @@ TableElement TableIndexSpec(boolean createContext): index = new Index().withType(createContext ? typeToken.image : null) .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) .withName(indexName).withUsing(using).withColumns(columns) - .withIndexSpec(indexOptions); + .withSpecOptions(indexOptions); } | typeToken= @@ -16358,7 +16369,7 @@ TableElement TableIndexSpec(boolean createContext): index = new Index().withType(createContext ? typeToken.image : null) .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) .withName(indexName).withUsing(using).withColumns(columns) - .withIndexSpec(indexOptions); + .withSpecOptions(indexOptions); } | (typeToken= | typeToken=) @@ -16372,7 +16383,7 @@ TableElement TableIndexSpec(boolean createContext): ? " " + keywordToken.image : "")) .withIndexKeyword(!createContext && keywordToken != null ? keywordToken.image : null) - .withName(indexName).withColumns(columns).withIndexSpec(indexOptions); + .withName(indexName).withColumns(columns).withSpecOptions(indexOptions); } ) { ((IndexOptionSource) index).setNullsDistinct(nullsDistinct); @@ -16381,8 +16392,13 @@ TableElement TableIndexSpec(boolean createContext): { return index; } } -void TableIndexOptions(boolean createContext, List options): +/** + * Index options after the key list, shared by CREATE TABLE and ALTER TABLE key definitions. + * CREATE TABLE also retains other accepted tokens as opaque options. + */ +void TableIndexOptions(boolean createContext, List options): { + IndexSpecOption option; List parameter; } { @@ -16393,9 +16409,18 @@ void TableIndexOptions(boolean createContext, List options): && getToken(1).kind != K_DEFERRABLE && !isKeywordAhead("INITIALLY") && !(getToken(1).kind == K_NOT && (getToken(2).kind == K_DEFERRABLE || "VALID".equalsIgnoreCase(getToken(2).image))) }) - parameter=CreateParameter() { options.addAll(parameter); } )* + ( + LOOKAHEAD(IndexSpecOption()) option=IndexSpecOption() { options.add(option); } + | + parameter=CreateParameter() { options.add(IndexSpecOption.raw(parameter)); } + ) + )* | - LOOKAHEAD({ !createContext }) IndexOptionList(options) + LOOKAHEAD({ !createContext }) + ( + LOOKAHEAD(IndexSpecOption(), { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) + option=IndexSpecOption() { options.add(option); } + )* ) } @@ -18217,7 +18242,9 @@ Drop Drop(): Table indexTable = null; int indexTablePosition = 0; List dropArgs = new ArrayList(); - List indexOption; + AlgorithmOrLockOption indexOption; + List indexOptions = new ArrayList(); + int indexOptionPosition = 0; boolean useTemporary = false; boolean foreign = false; boolean eventTrigger = false; @@ -18296,16 +18323,19 @@ Drop Drop(): [ "=" { drop.setEngineUseEquals(true); } ] engine=RelObjectName() { drop.setEngine(engine); } ] ( + // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, e.g. + // "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE". LOCK also starts a LOCK TABLE + // statement, so it is only taken here when it cannot begin the next statement. LOOKAHEAD({ getToken(1).kind == K_ALGORITHM || (getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE) }) - indexOption=IndexAlgorithmOrLockOption() { dropArgs.addAll(indexOption); } + indexOption=IndexAlgorithmOrLockOption() { + if (indexOptions.isEmpty()) { + indexOptionPosition = dropArgs.size(); + } + indexOptions.add(indexOption); + } | - ( - tk= - // MySQL DROP INDEX accepts a trailing algorithm_option / lock_option, - // e.g. "DROP INDEX i ON t ALGORITHM = INPLACE LOCK = NONE". - | tk= | tk= | tk="=" - ) { dropArgs.add(tk.image); } + tk= { dropArgs.add(tk.image); } | ( name = DropObjectName() { @@ -18318,11 +18348,6 @@ Drop Drop(): } } ) - | - // The lock_option of DROP INDEX. LOCK also starts a LOCK TABLE statement, so it is only - // taken as a DROP argument when it cannot be the beginning of the next statement. - LOOKAHEAD({ getToken(1).kind == K_LOCK && getToken(2).kind != K_TABLE }) - tk= { dropArgs.add(tk.image); } )* [ { behavior = CascadeBehavior.CASCADE; } | { behavior = CascadeBehavior.RESTRICT; } ] { @@ -18332,6 +18357,9 @@ Drop Drop(): if (indexTable != null) { drop.setTable(indexTable, indexTablePosition); } + if (!indexOptions.isEmpty()) { + drop.setAlgorithmOrLockOptions(indexOptions, indexOptionPosition); + } drop.setBehavior(behavior); drop.setUsingTemporary(useTemporary); @@ -18647,15 +18675,6 @@ void IndexWithComment(IndexOptionSource index): } } -void IndexOptionList(List list) : -{} -{ - ( - LOOKAHEAD(2, { !(getToken(1).kind == K_USING && getToken(2).kind == K_INDEX) }) - IndexOption(list) - )* -} - String UsingIndexType() : { String sk = null; @@ -18667,48 +18686,35 @@ String UsingIndexType() : } } -void IndexOption(List list) : +/** MySQL's index_option, shared by CREATE INDEX and the key definitions of CREATE/ALTER TABLE. */ +IndexSpecOption IndexSpecOption(): { - Token tk1 = null; - Token tk2 = null; - String sk1 = null; - boolean useEqual = false; - TableOption attribute; + Token token; + String value; + TableOption scalar; + IndexSpecOption option; } { ( - LOOKAHEAD({ isMySqlEngineAttributeAhead() }) attribute=MySqlScalarTableOption() - { list.addAll(attribute.getTokens()); } - | - tk1= ["=" { useEqual = true; } ] tk2= - { - list.add("KEY_BLOCK_SIZE" + (useEqual ? " = " : "") + tk2.image); - } - | - tk1= tk2= - { - list.add("WITH PARSER " + tk2.image); - } - | - tk1= tk2= - { - list.add("COMMENT " + tk2.image); - } - | - tk1= - { - list.add("VISIBLE"); - } - | - tk1= - { - list.add("INVISIBLE"); - } - | - sk1 = UsingIndexType(){ - list.add("USING " + sk1); - } + // KEY_BLOCK_SIZE and the engine attributes share the scalar syntax of table options. + LOOKAHEAD({ getToken(1).kind == K_KEY_BLOCK_SIZE || isMySqlEngineAttributeAhead() }) + scalar=MySqlScalarTableOption() { + option = IndexSpecOption.of(IndexSpecOption.Kind.valueOf(scalar.getKind().name()), + scalar.getValue(), scalar.isUseEquals()); + } + | + value=UsingIndexType() { option = IndexSpecOption.of(IndexSpecOption.Kind.INDEX_TYPE, value); } + | + value=RelObjectName() + { option = IndexSpecOption.of(IndexSpecOption.Kind.PARSER, value); } + | + token= { option = IndexSpecOption.comment(stringValue(token)); } + | + { option = IndexSpecOption.visibility(true); } + | + { option = IndexSpecOption.visibility(false); } ) + { return option; } } TablePartitioning CreateTablePartitioning(): @@ -19201,6 +19207,7 @@ void AlterExpressionAddConstraint(AlterExpression alterExp): List constraints = null; CheckConstraint checkCs = null; IndexOptions.Clustering clustering = null; + String using = null; } { @@ -19230,12 +19237,14 @@ void AlterExpressionAddConstraint(AlterExpression alterExp): | ( tk= tk2= clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX }) + using=UsingIndexType() ] columnNames=ColumnsNamesList() { index = new KeyConstraint() .withName(sk3) .withType(tk.image + " " + tk2.image) - .withClustering(clustering) + .withClustering(clustering).withUsing(using) .withColumnsNames(columnNames); alterExp.setConstraint(index); } @@ -19260,12 +19269,14 @@ void AlterExpressionAddConstraint(AlterExpression alterExp): ( tk= (tk2= | tk2=)? clustering=SqlServerIndexClustering() + [ LOOKAHEAD({ getToken(1).kind == K_USING && getToken(2).kind != K_INDEX }) + using=UsingIndexType() ] columnNames=ColumnsNamesList() { index = new KeyConstraint() .withName(sk3) .withType(tk.image + (tk2!=null?" " + tk2.image:"")) - .withClustering(clustering) + .withClustering(clustering).withUsing(using) .withColumnsNames(columnNames); alterExp.setConstraint(index); } @@ -19479,7 +19490,7 @@ AlterExpression AlterExpressionAddAlterModify(): TableElement element; AlterExpression.ColumnDataType alterExpressionColumnDataType = null; AlterExpression.ColumnDropNotNull alterExpressionColumnDropNotNull = null; - List indexSpec = new ArrayList(); + List indexSpec = new ArrayList(); List partitionDefinition = null; } { @@ -19501,9 +19512,9 @@ AlterExpression AlterExpressionAddAlterModify(): | LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ALTER && getToken(1).kind == K_INDEX }) - tk= sk3=RelObjectName() IndexOptionList(indexSpec) { + tk= sk3=RelObjectName() TableIndexOptions(false, indexSpec) { index = new Index().withIndexKeyword(tk.image).withName(sk3) - .withKind(Index.Kind.INDEX).withIndexSpec(indexSpec); + .withKind(Index.Kind.INDEX).withSpecOptions(indexSpec); alterExp.setIndex(index); } | diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index fac9650c5..2f7fc5f2f 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -293,10 +293,11 @@ The legacy accessors remain and are derived from these properties: the argument declarations of the last routine, where the legacy single-signature form kept them. The map now also covers PROCEDURE, ROUTINE and AGGREGATE. -* ``getParameters()`` is a snapshot that includes ON with the owner table and - the CASCADE or RESTRICT keyword. ``setParameters()`` replaces the clause with - raw tokens and clears both; ``addParameters()`` keeps them. Raw tokens are not - parsed: ``addParameters("CASCADE")`` leaves ``getBehavior()`` null. +* ``getParameters()`` is a snapshot that includes ON with the owner table, the + MySQL ALGORITHM and LOCK clauses and the CASCADE or RESTRICT keyword, in + source order. ``setParameters()`` replaces the clause with raw tokens and + clears all three; ``addParameters()`` keeps them. Raw tokens are not parsed: + ``addParameters("CASCADE")`` leaves ``getBehavior()`` null. * ``setName()``, ``setNames()``, ``addNames()`` and ``setTypeToParameters()`` first convert routine and account targets to the legacy names and single signature, so ``setName()`` on ``DROP FUNCTION f(int)`` still renders @@ -328,7 +329,8 @@ setters do not parse SQL or infer a table. Table discovery and statement visitors traverse DROP TABLE/VIEW targets and explicit index owners, without reporting catalog-only object names as tables. The statement deparser delegates real tables to its configured select deparser. -MySQL ALGORITHM/LOCK tokens retain their order and optional equals signs. +MySQL ALGORITHM and LOCK clauses retain their order and optional equals signs as +typed nodes; see :ref:`Share MySQL index options`. MySQL named constraint drops ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -468,6 +470,73 @@ Use ``TableOption.onCommit`` and ``TableOption.accessMethod`` to build them. .setOnCommitAction(TableOption.OnCommitAction.PRESERVE_ROWS); // CREATE TEMP TABLE t (id INT) USING heap ON COMMIT PRESERVE ROWS +Share MySQL index options +------------------------- + +CREATE INDEX, the index and key definitions of CREATE TABLE, and ALTER TABLE +``ADD`` or ``ALTER INDEX`` parse MySQL index options with one grammar rule into +one model. ``IndexOptions.getSpecOptions()``, also available on ``Index`` and +``KeyConstraint``, returns the options written after the key list as mutable, +ordered ``IndexSpecOption`` nodes. ``getKind()`` selects the payload: + +* ``KEY_BLOCK_SIZE``, ``ENGINE_ATTRIBUTE`` and ``SECONDARY_ENGINE_ATTRIBUTE``: + ``getValue()`` and the optional ``=`` reported by ``isUseEquals()`` +* ``INDEX_TYPE`` (``USING BTREE``) and ``PARSER`` (``WITH PARSER ngram``): + ``getValue()`` +* ``COMMENT``: ``getComment()`` returns the literal as a ``StringValue`` that + records how the dialect escapes it, so ``getNotExcapedValue()`` returns the + stored text, for example ``it's`` for ``'it''s'`` or, with MySQL backslash + escapes, ``'it\'s'``. ``getValue()`` remains the literal's SQL text. +* ``VISIBILITY``: ``getVisible()`` for VISIBLE or INVISIBLE +* ``OTHER``: opaque tokens that CREATE TABLE and CREATE INDEX accept for other + dialects + +Values keep their SQL spelling, including the quotes of string literals. An +index type written before the key list, as in ``KEY k USING BTREE (a)``, +``PRIMARY KEY USING BTREE (id)`` or ``CREATE INDEX k USING HASH ON t (a)``, +remains ``getUsing()``; ``CreateIndex.isIndexTypeBeforeOn()`` still distinguishes +the two CREATE INDEX positions. After the key list it is an ``INDEX_TYPE`` option +at its source position. PostgreSQL's INCLUDE, NULLS DISTINCT, storage parameters +and tablespace keep their dedicated properties. + +CREATE INDEX keeps these options on ``getIndex()``. Its ALGORITHM and LOCK +clauses are ``AlgorithmOrLockOption`` nodes in +``CreateIndex.getAlgorithmOrLockOptions()``; DROP INDEX exposes the same type +through ``Drop.getAlgorithmOrLockOptions()``. CREATE INDEX renders them after the +index options, as MySQL requires. + +.. code-block:: java + + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse( + "CREATE UNIQUE INDEX uk ON t (a) USING BTREE COMMENT 'x' ALGORITHM = INPLACE", + parser -> parser.withDialect(Dialect.MYSQL)); + List options = create.getIndex().getSpecOptions(); + options.get(0).setValue("HASH"); // INDEX_TYPE + options.add(IndexSpecOption.visibility(false)); + create.getAlgorithmOrLockOptions().get(0).setValue("COPY"); + // CREATE UNIQUE INDEX uk ON t (a) USING HASH COMMENT 'x' INVISIBLE ALGORITHM = COPY + +The kind of an option is fixed. Setters for a payload that the kind does not have, +and ``setTokens`` on a typed option, throw ``IllegalStateException``; replace the +node to change its kind. ``IndexSpecOption.raw`` copies its tokens and never +parses them. + +The legacy accessors are derived from these nodes. ``getIndexSpec()`` returns an +unmodifiable token snapshot of ``getSpecOptions()``, or null when the options are +unset. ``CreateIndex.getTailParameters()`` returns the tokens of the index options +followed by ALGORITHM and LOCK, and ``Drop.getParameters()`` includes ALGORITHM +and LOCK at their source position. ``setIndexSpec`` and +``CreateIndex.setTailParameters`` replace these clauses with one opaque option; +``Drop.setParameters`` also clears ALGORITHM and LOCK. + +Migration: edits of the ``getIndexSpec()`` and ``getTailParameters()`` snapshots +now throw ``UnsupportedOperationException``; edit the nodes or call the setters. +ALTER TABLE key definitions report the same tokens as CREATE TABLE, for example +``KEY_BLOCK_SIZE``, ``=`` and ``8`` instead of the single entry +``KEY_BLOCK_SIZE = 8``. For a constructed statement without trailing clauses, +``getTailParameters()`` returns an empty list instead of null. Typed option +keywords are rendered in upper case. + Inspect PostgreSQL schema statements ------------------------------------ diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java index 04ee07179..55baedf75 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java @@ -40,6 +40,7 @@ import net.sf.jsqlparser.statement.create.table.CheckConstraint; import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption; import net.sf.jsqlparser.statement.create.table.KeyElement; import net.sf.jsqlparser.statement.create.table.KeyConstraint; import net.sf.jsqlparser.statement.create.table.PartitionDefinition; @@ -2072,13 +2073,14 @@ public void testAlterTableAddFullTextIndexWithOptions() throws JSQLParserExcepti assertEquals("col1", columnNames.get(0)); assertEquals("col2", columnNames.get(1)); - List indexSpec = index.getIndexSpec(); - assertNotNull(indexSpec); - assertEquals(4, indexSpec.size()); - assertEquals("KEY_BLOCK_SIZE = 8", indexSpec.get(0)); - assertEquals("WITH PARSER ngram", indexSpec.get(1)); - assertEquals("COMMENT 'fulltext'", indexSpec.get(2)); - assertEquals("INVISIBLE", indexSpec.get(3)); + List options = index.getSpecOptions(); + assertEquals(4, options.size()); + assertEquals("KEY_BLOCK_SIZE = 8", options.get(0).toString()); + assertEquals("WITH PARSER ngram", options.get(1).toString()); + assertEquals("COMMENT 'fulltext'", options.get(2).toString()); + assertEquals("INVISIBLE", options.get(3).toString()); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8", "WITH", "PARSER", "ngram", "COMMENT", + "'fulltext'", "INVISIBLE"), index.getIndexSpec()); assertSqlCanBeParsedAndDeparsed(sql); } @@ -2134,13 +2136,14 @@ public void testAlterTableAddIndexWithOptions() throws JSQLParserException { assertEquals("idx_lastname", index.getName()); assertEquals("last_name", index.getColumnsNames().get(0)); - List indexSpec = index.getIndexSpec(); - assertNotNull(indexSpec); - assertEquals(4, indexSpec.size()); - assertEquals("USING BTREE", indexSpec.get(0)); - assertEquals("KEY_BLOCK_SIZE = 16", indexSpec.get(1)); - assertEquals("COMMENT 'Performance tuning'", indexSpec.get(2)); - assertEquals("VISIBLE", indexSpec.get(3)); + List options = index.getSpecOptions(); + assertEquals(4, options.size()); + assertEquals("USING BTREE", options.get(0).toString()); + assertEquals("KEY_BLOCK_SIZE = 16", options.get(1).toString()); + assertEquals("COMMENT 'Performance tuning'", options.get(2).toString()); + assertEquals("VISIBLE", options.get(3).toString()); + assertEquals(List.of("USING", "BTREE", "KEY_BLOCK_SIZE", "=", "16", "COMMENT", + "'Performance tuning'", "VISIBLE"), index.getIndexSpec()); assertSqlCanBeParsedAndDeparsed(sql); } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java new file mode 100644 index 000000000..4ea4f93b1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlIndexSpecOptionTest.java @@ -0,0 +1,421 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.CascadeBehavior; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.create.index.AlgorithmOrLockOption; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.IndexOptionSource; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption; +import net.sf.jsqlparser.statement.create.table.IndexSpecOption.Kind; +import net.sf.jsqlparser.statement.create.table.KeyConstraint; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlIndexSpecOptionTest { + private static final String OPTIONS = "KEY_BLOCK_SIZE = 8 USING HASH WITH PARSER ngram " + + "COMMENT 'c' INVISIBLE ENGINE_ATTRIBUTE '{}' SECONDARY_ENGINE_ATTRIBUTE = '{}'"; + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void createIndexKeepsIndexOptionsOnItsIndexAndAlgorithmLockOnTheStatement(boolean mysql) + throws Exception { + String sql = "CREATE UNIQUE INDEX uk ON t1 (a DESC, b (10)) USING BTREE COMMENT 'x' " + + "ALGORITHM = INPLACE LOCK = NONE"; + CreateIndex create = (CreateIndex) roundTrip(sql, mysql); + Index index = create.getIndex(); + + assertNull(index.getUsing()); + assertOptions(index, "INDEX_TYPE BTREE", "COMMENT 'x'"); + assertEquals(2, create.getAlgorithmOrLockOptions().size()); + assertAlgorithmOrLock(create.getAlgorithmOrLockOptions().get(0), + AlgorithmOrLockOption.Kind.ALGORITHM, "INPLACE"); + assertAlgorithmOrLock(create.getAlgorithmOrLockOptions().get(1), + AlgorithmOrLockOption.Kind.LOCK, "NONE"); + assertEquals(List.of("USING", "BTREE", "COMMENT", "'x'"), index.getIndexSpec()); + assertEquals(List.of("USING", "BTREE", "COMMENT", "'x'", "ALGORITHM", "=", "INPLACE", + "LOCK", "=", "NONE"), create.getTailParameters()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "CREATE FULLTEXT INDEX ft ON t1 (body) WITH PARSER ngram|PARSER ngram", + "CREATE INDEX i ON t1 (a) INVISIBLE|VISIBILITY false", + "CREATE INDEX i ON t1 (a) VISIBLE|VISIBILITY true", + "CREATE INDEX i ON db1.t1 (a, b) KEY_BLOCK_SIZE = 8|KEY_BLOCK_SIZE 8 =", + "CREATE INDEX i ON t1 (a) KEY_BLOCK_SIZE 8 ENGINE_ATTRIBUTE '{}'" + + "|KEY_BLOCK_SIZE 8,ENGINE_ATTRIBUTE '{}'"}) + void createIndexOptionsAreTyped(String sql, String expected) throws Exception { + for (boolean mysql : List.of(true, false)) { + CreateIndex create = (CreateIndex) roundTrip(sql, mysql); + assertOptions(create.getIndex(), expected.split(",")); + assertTrue(create.getAlgorithmOrLockOptions().isEmpty()); + } + } + + @Test + void indexTypeKeepsItsPositionRelativeToTheKeyList() throws Exception { + CreateIndex beforeOn = (CreateIndex) roundTrip("CREATE INDEX i USING HASH ON t1 (a)", true); + assertEquals("HASH", beforeOn.getIndex().getUsing()); + assertTrue(beforeOn.isIndexTypeBeforeOn()); + assertNull(beforeOn.getIndex().getSpecOptions()); + + CreateTable table = (CreateTable) roundTrip("CREATE TABLE t20 (a INT, b VARCHAR (100), " + + "KEY idx_a USING BTREE (a) KEY_BLOCK_SIZE = 4 COMMENT 'idx', " + + "UNIQUE INDEX uk_b (b (20)) USING HASH)", true); + Index key = table.getIndexes().get(0); + assertEquals("BTREE", key.getUsing()); + assertOptions(key, "KEY_BLOCK_SIZE 4 =", "COMMENT 'idx'"); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "4", "COMMENT", "'idx'"), key.getIndexSpec()); + KeyConstraint unique = (KeyConstraint) table.getTableConstraints().get(0); + assertNull(unique.getUsing()); + assertOptions(unique, "INDEX_TYPE HASH"); + assertEquals(List.of("USING", "HASH"), unique.getIndexSpec()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t1 ADD CONSTRAINT pk_t1 PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD CONSTRAINT uk UNIQUE KEY uk_idx USING BTREE (id)", + "CREATE TABLE t1 (id INT, PRIMARY KEY USING BTREE (id) COMMENT 'pk')", + "CREATE TABLE t1 (id INT, CONSTRAINT pk_t1 PRIMARY KEY USING HASH (id))"}) + void keysAcceptTheIndexTypeBeforeTheKeyList(String sql) throws Exception { + KeyConstraint key = keyConstraint(roundTrip(sql, true)); + assertEquals(sql.contains("HASH") ? "HASH" : "BTREE", key.getUsing()); + assertEquals(List.of("id"), key.getColumnsNames()); + assertFalse(key.getSpecOptions().stream() + .anyMatch(option -> option.getKind() == Kind.INDEX_TYPE)); + } + + @Test + void defaultModeAcceptsTheIndexTypeBeforeNamedConstraintKeys() throws Exception { + for (String sql : List.of( + "ALTER TABLE t1 ADD CONSTRAINT pk_t1 PRIMARY KEY USING BTREE (id)", + "ALTER TABLE t1 ADD CONSTRAINT uk UNIQUE KEY USING BTREE (id)")) { + Statement statement = roundTrip(sql, false); + KeyConstraint key = keyConstraint(statement); + assertEquals("BTREE", key.getUsing()); + key.setUsing("HASH"); + assertRendered(statement, sql.replace("BTREE", "HASH")); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE INDEX ix ON t (a) %s", + "CREATE TABLE t (a INT, KEY ix (a) %s)", + "CREATE TABLE t (a INT, UNIQUE KEY ix (a) %s)", + "ALTER TABLE t ADD INDEX ix (a) %s", "ALTER TABLE t ADD UNIQUE KEY ix (a) %s", + "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (a) %s"}) + void everyDefinitionSharesTheSameOptionModel(String template) throws Exception { + IndexOptionSource source = definition(roundTrip(String.format(template, OPTIONS), true)); + assertOptions(source, "KEY_BLOCK_SIZE 8 =", "INDEX_TYPE HASH", "PARSER ngram", + "COMMENT 'c'", "VISIBILITY false", "ENGINE_ATTRIBUTE '{}'", + "SECONDARY_ENGINE_ATTRIBUTE '{}' ="); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8", "USING", "HASH", "WITH", "PARSER", + "ngram", "COMMENT", "'c'", "INVISIBLE", "ENGINE_ATTRIBUTE", "'{}'", + "SECONDARY_ENGINE_ATTRIBUTE", "=", "'{}'"), source.getIndexSpec()); + } + + @Test + void alterKeyBlockSizeWithoutEqualsKeepsItsSeparator() throws Exception { + // ALTER used to render this option as KEY_BLOCK_SIZE8 + Alter alter = (Alter) roundTrip("ALTER TABLE t ADD INDEX i (a) KEY_BLOCK_SIZE 8", true); + assertOptions(alter.getAlterExpressions().get(0).getIndex(), "KEY_BLOCK_SIZE 8"); + } + + @Test + void commentsAreStringValuesThatDecodeTheStoredText() throws Exception { + String[][] comments = {{"'it''s'", "it's"}, {"'it\\'s'", "it's"}, {"\"dq\"", "dq"}, + {"'a\\\\b'", "a\\b"}, {"'인덱스 설명'", "인덱스 설명"}}; + for (String template : List.of("CREATE INDEX ix ON t (a) COMMENT %s", + "CREATE TABLE t (a INT, KEY ix (a) COMMENT %s)", + "ALTER TABLE t ADD INDEX ix (a) COMMENT %s")) { + for (String[] comment : comments) { + String sql = String.format(template, comment[0]); + IndexSpecOption option = + definition(roundTrip(sql, true)).getSpecOptions().get(0); + assertEquals(Kind.COMMENT, option.getKind()); + assertEquals(comment[0], option.getValue(), sql); + assertEquals(comment[0], option.getComment().toString(), sql); + assertEquals(comment[1], option.getComment().getNotExcapedValue(), sql); + } + } + + Alter alter = (Alter) roundTrip("ALTER TABLE t ADD INDEX ix (a) COMMENT 'it\\'s'", true); + IndexSpecOption comment = alter.getAlterExpressions().get(0).getIndex() + .getSpecOptions().get(0); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, comment.getComment().getEscaping()); + comment.setValue("'don\\'t'"); + assertEquals("don't", comment.getComment().getNotExcapedValue()); + comment.setComment(new StringValue("'changed'")); + assertRendered(alter, "ALTER TABLE t ADD INDEX ix (a) COMMENT 'changed'"); + assertEquals(List.of("COMMENT", "'changed'"), + alter.getAlterExpressions().get(0).getIndex().getIndexSpec()); + } + + @Test + void typedEditsReachBothRenderers() throws Exception { + Alter alter = (Alter) roundTrip( + "ALTER TABLE t1 ADD INDEX i3 (a) USING BTREE COMMENT 'c' INVISIBLE", true); + List options = alter.getAlterExpressions().get(0).getIndex() + .getSpecOptions(); + options.get(0).setValue("HASH"); + options.get(1).setValue("'changed'"); + options.get(2).setVisible(true); + options.add(0, IndexSpecOption.of(Kind.KEY_BLOCK_SIZE, "16", true)); + assertRendered(alter, "ALTER TABLE t1 ADD INDEX i3 (a) KEY_BLOCK_SIZE = 16 USING HASH " + + "COMMENT 'changed' VISIBLE"); + + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) LOCK EXCLUSIVE ALGORITHM DEFAULT", true); + create.getAlgorithmOrLockOptions().get(0).setValue("SHARED"); + create.getAlgorithmOrLockOptions().get(1).setUseEquals(true); + create.getIndex().getIndexOptions().addSpecOptions(IndexSpecOption.of(Kind.COMMENT, "'x'")); + assertRendered(create, + "CREATE INDEX i ON t (a) COMMENT 'x' LOCK SHARED ALGORITHM = DEFAULT"); + assertEquals(List.of("COMMENT", "'x'", "LOCK", "SHARED", "ALGORITHM", "=", "DEFAULT"), + create.getTailParameters()); + } + + @Test + void legacyAccessorsAreSnapshotsAndSettersReplaceWithOpaqueOptions() throws Exception { + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) parallel compress nologging", false); + assertEquals(List.of("parallel", "compress", "nologging"), create.getTailParameters()); + assertTrue(create.getIndex().getSpecOptions().stream() + .allMatch(option -> option.getKind() == Kind.OTHER)); + assertThrows(UnsupportedOperationException.class, + () -> create.getTailParameters().add("x")); + assertThrows(UnsupportedOperationException.class, + () -> create.getIndex().getIndexSpec().add("x")); + + create.addAlgorithmOrLockOptions(AlgorithmOrLockOption.lock("NONE")); + List tail = new ArrayList<>(List.of("KEY_BLOCK_SIZE", "=", "4")); + create.setTailParameters(tail); + tail.clear(); + assertTrue(create.getAlgorithmOrLockOptions().isEmpty()); + assertEquals(Kind.OTHER, create.getIndex().getSpecOptions().get(0).getKind()); + assertRendered(create, "CREATE INDEX i ON t (a) KEY_BLOCK_SIZE = 4"); + + Index index = new Index().withName("i").withColumnsNames(List.of("a")); + assertNull(index.getIndexSpec()); + index.setIndexSpec(List.of("USING", "BTREE")); + assertEquals(List.of("USING", "BTREE"), index.getIndexSpec()); + assertEquals("i (a) USING BTREE", index.toString()); + index.setIndexSpec(List.of()); + assertTrue(index.getSpecOptions().isEmpty()); + index.setIndexSpec(null); + assertNull(index.getSpecOptions()); + assertEquals(List.of(), new CreateIndex().getTailParameters()); + } + + @Test + void typedOptionsRejectPayloadsOfOtherKinds() { + IndexSpecOption comment = IndexSpecOption.comment(new StringValue("'c'")); + assertEquals("'c'", comment.getValue()); + assertThrows(IllegalStateException.class, + () -> IndexSpecOption.of(Kind.PARSER, "ngram").setComment(new StringValue("'x'"))); + assertNull(IndexSpecOption.of(Kind.PARSER, "ngram").getComment()); + assertThrows(NullPointerException.class, () -> comment.setComment(null)); + assertThrows(IllegalStateException.class, () -> comment.setTokens(List.of("x"))); + assertThrows(IllegalStateException.class, () -> comment.setUseEquals(true)); + assertThrows(IllegalStateException.class, () -> comment.setVisible(true)); + assertNull(comment.getVisible()); + IndexSpecOption visible = IndexSpecOption.visibility(true); + assertThrows(IllegalStateException.class, () -> visible.setValue("x")); + assertEquals(List.of("VISIBLE"), visible.getTokens()); + assertThrows(IllegalStateException.class, + () -> IndexSpecOption.of(Kind.OTHER, "x")); + assertThrows(NullPointerException.class, () -> IndexSpecOption.of(Kind.PARSER, null)); + + List tokens = new ArrayList<>(List.of("ON", "[PRIMARY]")); + IndexSpecOption raw = IndexSpecOption.raw(tokens); + tokens.set(1, "ignored"); + assertEquals("ON [PRIMARY]", raw.toString()); + assertThrows(UnsupportedOperationException.class, () -> raw.getTokens().add("x")); + assertThrows(IllegalStateException.class, () -> raw.setValue("x")); + raw.setTokens(List.of("ON", "fg")); + assertEquals(List.of("ON", "fg"), raw.getTokens()); + } + + @Test + void dropIndexAlgorithmAndLockAreTyped() throws Exception { + Drop drop = (Drop) roundTrip("DROP INDEX idx ON t1 ALGORITHM = INPLACE LOCK = NONE", true); + assertEquals("t1", drop.getTable().getName()); + assertAlgorithmOrLock(drop.getAlgorithmOrLockOptions().get(0), + AlgorithmOrLockOption.Kind.ALGORITHM, "INPLACE"); + assertAlgorithmOrLock(drop.getAlgorithmOrLockOptions().get(1), + AlgorithmOrLockOption.Kind.LOCK, "NONE"); + assertEquals(List.of("ON", "t1", "ALGORITHM", "=", "INPLACE", "LOCK", "=", "NONE"), + drop.getParameters()); + + drop.getAlgorithmOrLockOptions().remove(1); + drop.getAlgorithmOrLockOptions().get(0).setValue("COPY"); + drop.setBehavior(CascadeBehavior.RESTRICT); + assertRendered(drop, "DROP INDEX idx ON t1 ALGORITHM = COPY RESTRICT"); + assertEquals(List.of("ON", "t1", "ALGORITHM", "=", "COPY", "RESTRICT"), + drop.getParameters()); + + drop.addParameters("legacy"); + drop.setAlgorithmOrLockOptions(List.of(AlgorithmOrLockOption.lock("SHARED"))); + assertRendered(drop, "DROP INDEX idx ON t1 legacy LOCK = SHARED RESTRICT"); + drop.setAlgorithmOrLockOptions(List.of(AlgorithmOrLockOption.lock("SHARED")), 0); + assertEquals(List.of("ON", "t1", "LOCK", "=", "SHARED", "legacy", "RESTRICT"), + drop.getParameters()); + assertThrows(IllegalArgumentException.class, + () -> drop.setAlgorithmOrLockOptions(List.of(), 2)); + + drop.setParameters(List.of("CASCADE")); + assertTrue(drop.getAlgorithmOrLockOptions().isEmpty()); + assertNull(drop.getBehavior()); + assertNull(drop.getTable()); + assertEquals("DROP INDEX idx CASCADE", drop.toString()); + + Drop single = (Drop) roundTrip("DROP INDEX idx ON t1 ALGORITHM = INPLACE", false); + assertEquals(1, single.getAlgorithmOrLockOptions().size()); + } + + @Test + void replacingTheCreateIndexDefinitionKeepsOrInheritsItsOptions() throws Exception { + CreateIndex create = (CreateIndex) roundTrip( + "CREATE INDEX i ON t (a) KEY_BLOCK_SIZE = 8", true); + Index removed = create.getIndex(); + IndexSpecOption keyBlockSize = removed.getSpecOptions().get(0); + create.setIndex(null); + assertEquals(List.of("KEY_BLOCK_SIZE", "=", "8"), create.getTailParameters()); + + create.setIndex(new Index().withName("j").withColumnsNames(List.of("b"))); + assertSame(keyBlockSize, create.getIndex().getSpecOptions().get(0)); + assertNotSame(removed.getSpecOptions(), create.getIndex().getSpecOptions()); + assertRendered(create, "CREATE INDEX j ON t (b) KEY_BLOCK_SIZE = 8"); + + create.setIndex(new Index().withName("k").withColumnsNames(List.of("c")) + .withSpecOptions(List.of(IndexSpecOption.visibility(false)))); + assertRendered(create, "CREATE INDEX k ON t (c) INVISIBLE"); + } + + @Test + void postgreSqlIndexOptionsKeepTheirProperties() throws Exception { + String sql = "CREATE UNIQUE INDEX CONCURRENTLY ix ON ONLY t USING btree (id) " + + "INCLUDE (v) NULLS NOT DISTINCT WITH (fillfactor = 80) TABLESPACE fast " + + "WHERE active"; + CreateIndex create = (CreateIndex) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals(sql, create.toString()); + assertEquals("btree", create.getIndex().getUsing()); + assertEquals(List.of("v"), create.getIncludeColumns()); + assertEquals("fast", create.getTableSpace()); + assertNull(create.getIndex().getSpecOptions()); + assertTrue(create.getTailParameters().isEmpty()); + + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CONSTRAINT uk UNIQUE (id) " + + "INCLUDE (v) WITH (fillfactor = 70) USING INDEX TABLESPACE fast", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + KeyConstraint key = alter.getAlterExpressions().get(0).getKeyConstraint(); + assertEquals("fast", key.getTableSpace()); + assertEquals(1, key.getStorageParameters().size()); + assertTrue(key.getSpecOptions().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER TABLE t ADD PRIMARY KEY USING (id)", + "ALTER TABLE t ADD PRIMARY KEY USING BTREE USING HASH (id)", + "ALTER TABLE t ADD INDEX i (a) KEY_BLOCK_SIZE", + "ALTER TABLE t ADD INDEX i (a) WITH PARSER", + "ALTER TABLE t ADD INDEX i (a) COMMENT", + "CREATE INDEX i ON t (a) LOCK = NONE ALGORITHM"}) + void rejectsIncompleteOptions(String sql) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.MYSQL))); + } + + private static void assertOptions(IndexOptionSource source, String... expected) { + List actual = new ArrayList<>(); + for (IndexSpecOption option : source.getSpecOptions()) { + Object payload = option.getKind() == Kind.VISIBILITY ? option.getVisible() + : option.getValue(); + actual.add(option.getKind() + " " + payload + (option.isUseEquals() ? " =" : "")); + } + assertEquals(List.of(expected), actual); + } + + private static void assertAlgorithmOrLock(AlgorithmOrLockOption option, + AlgorithmOrLockOption.Kind kind, String value) { + assertEquals(kind, option.getKind()); + assertEquals(value, option.getValue()); + assertTrue(option.isUseEquals()); + } + + private static IndexOptionSource definition(Statement statement) { + if (statement instanceof CreateIndex) { + return ((CreateIndex) statement).getIndex(); + } + if (statement instanceof CreateTable) { + CreateTable table = (CreateTable) statement; + return (IndexOptionSource) (table.getIndexes().isEmpty() + ? table.getTableConstraints().get(0) + : table.getIndexes().get(0)); + } + AlterExpression action = ((Alter) statement).getAlterExpressions().get(0); + return (IndexOptionSource) (action.getConstraint() != null ? action.getConstraint() + : action.getIndex()); + } + + private static KeyConstraint keyConstraint(Statement statement) { + return (KeyConstraint) definition(statement); + } + + private static Statement roundTrip(String sql, boolean mysql) throws Exception { + Consumer dialect = mysql ? parser -> parser.withDialect(Dialect.MYSQL) + : parser -> { + }; + Statement statement = CCJSqlParserUtil.parse(sql, dialect); + assertEquals(sql, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(sql, deparsed.toString()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, dialect).toString()); + return statement; + } + + private static void assertRendered(Statement statement, String expected) throws Exception { + assertEquals(expected, statement.toString()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed)); + assertEquals(expected, deparsed.toString()); + assertEquals(expected, CCJSqlParserUtil.parse(expected, + parser -> parser.withDialect(Dialect.MYSQL)).toString()); + } +}