From a4b0bfe5ec625ca8abbc00c11b1877cddb67689a Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 18:01:52 +0900 Subject: [PATCH 1/4] refactor(ast): model structured table definitions --- .../statement/alter/AlterExpression.java | 6 +- .../statement/create/table/ColDataType.java | 84 ++++++++++- .../create/table/ColumnDefinition.java | 52 ++++++- .../statement/create/table/ColumnOption.java | 70 +++++++++ .../statement/create/table/CreateTable.java | 88 +++++++++++- .../create/table/ForeignKeyIndex.java | 72 +++++++++- .../create/table/ForeignKeyReference.java | 135 ++++++++++++++++++ .../statement/create/table/Index.java | 48 ++++++- .../create/table/NamedConstraint.java | 6 + .../statement/create/table/TableElement.java | 22 +++ .../statement/create/table/TableOption.java | 132 +++++++++++++++++ 11 files changed, 696 insertions(+), 19 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java index 4055eb8b1..35c89c578 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java @@ -1163,6 +1163,10 @@ protected void toStringGeneral(StringBuilder b) { b.append("IF EXISTS "); } b.append(constraintName); + } else if (index != null) { + // The structured index is canonical. Legacy PK/UK/FK fields may also be populated for + // source compatibility, but cannot represent names, expressions, or index options. + b.append(index); } else if (pkColumns != null) { b.append("PRIMARY KEY (").append(PlainSelect.getStringList(pkColumns)).append(')'); } else if (ukColumns != null) { @@ -1195,8 +1199,6 @@ protected void toStringGeneral(StringBuilder b) { .append(PlainSelect.getStringList(fkSourceColumns)) .append(")"); referentialActions.forEach(b::append); - } else if (index != null) { - b.append(index); } if (getConstraints() != null && !getConstraints().isEmpty()) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java index c81317d76..c41c713ec 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java @@ -28,6 +28,14 @@ public enum Signedness { SIGNED, UNSIGNED } + public enum TypeModifier { + SIGNED, UNSIGNED, ZEROFILL + } + + public enum NationalCharacterType { + CHAR, VARCHAR + } + private String dataType; private List argumentsStringList; private String characterSet; @@ -37,6 +45,8 @@ public enum Signedness { private boolean zerofill; private Integer precision; private Integer scale; + private List typeModifiers; + private NationalCharacterType nationalCharacterType; public ColDataType() { // empty constructor @@ -120,6 +130,58 @@ public void setZerofill(boolean zerofill) { this.zerofill = zerofill; } + /** Returns MySQL numeric modifiers in their original order, including repetitions. */ + public List getTypeModifiers() { + return typeModifiers; + } + + public void setTypeModifiers(List typeModifiers) { + this.typeModifiers = typeModifiers; + signedness = null; + zerofill = false; + if (typeModifiers != null) { + for (TypeModifier modifier : typeModifiers) { + updateEffectiveModifier(modifier); + } + } + } + + public void addTypeModifier(TypeModifier typeModifier) { + if (typeModifiers == null) { + typeModifiers = new ArrayList<>(); + } + typeModifiers.add(typeModifier); + updateEffectiveModifier(typeModifier); + } + + private void updateEffectiveModifier(TypeModifier modifier) { + switch (modifier) { + case SIGNED: + signedness = Signedness.SIGNED; + break; + case UNSIGNED: + signedness = Signedness.UNSIGNED; + break; + case ZEROFILL: + zerofill = true; + break; + default: + break; + } + } + + public NationalCharacterType getNationalCharacterType() { + return nationalCharacterType; + } + + public void setNationalCharacterType(NationalCharacterType nationalCharacterType) { + this.nationalCharacterType = nationalCharacterType; + } + + public boolean isNational() { + return nationalCharacterType != null; + } + /** * The first numeric type parameter, e.g. {@code 255} for {@code VARCHAR(255)} or {@code 10} for * {@code DECIMAL(10, 2)}. {@code MAX} is reported as {@link Integer#MAX_VALUE}. Returns @@ -161,8 +223,10 @@ public String toString() { + (argumentsStringList != null ? " " + PlainSelect.getStringList(argumentsStringList, true, true) : "") - + (signedness != null ? " " + signedness : "") - + (zerofill ? " ZEROFILL" : "") + + (typeModifiers != null && !typeModifiers.isEmpty() + ? " " + PlainSelect.getStringList(typeModifiers, false, false) + : (signedness != null ? " " + signedness : "") + + (zerofill ? " ZEROFILL" : "")) + arraySpec.toString() + (characterSet != null ? " CHARACTER SET " + characterSet : ""); } @@ -202,6 +266,16 @@ public ColDataType withZerofill(boolean zerofill) { return this; } + public ColDataType withTypeModifiers(List typeModifiers) { + setTypeModifiers(typeModifiers); + return this; + } + + public ColDataType withNationalCharacterType(NationalCharacterType nationalCharacterType) { + setNationalCharacterType(nationalCharacterType); + return this; + } + public ColDataType withPrecision(Integer precision) { this.setPrecision(precision); return this; @@ -254,7 +328,9 @@ public final boolean equals(Object o) { && Objects.equals(intervalQualifier, that.intervalQualifier) && Objects.equals(arrayData, that.arrayData) && signedness == that.signedness - && zerofill == that.zerofill; + && zerofill == that.zerofill + && Objects.equals(typeModifiers, that.typeModifiers) + && nationalCharacterType == that.nationalCharacterType; } @Override @@ -266,6 +342,8 @@ public int hashCode() { result = 31 * result + Objects.hashCode(arrayData); result = 31 * result + Objects.hashCode(signedness); result = 31 * result + Boolean.hashCode(zerofill); + result = 31 * result + Objects.hashCode(typeModifiers); + result = 31 * result + Objects.hashCode(nationalCharacterType); return result; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java index a7f47104c..239ffb07c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java @@ -22,11 +22,12 @@ /** * Globally used definition class for columns. */ -public class ColumnDefinition implements ImportColumn, Serializable { +public class ColumnDefinition implements ImportColumn, TableElement, Serializable { private String columnName; private ColDataType colDataType; private List columnSpecs; + private List columnOptions; public ColumnDefinition() {} @@ -46,6 +47,47 @@ public List getColumnSpecs() { public void setColumnSpecs(List list) { columnSpecs = list; + columnOptions = null; + } + + /** + * Returns column options in source order, including structured references and MySQL + * {@code SERIAL DEFAULT VALUE}. + */ + public List getColumnOptions() { + return columnOptions; + } + + public void setColumnOptions(List columnOptions) { + this.columnOptions = columnOptions; + } + + public boolean isSerialDefaultValue() { + return columnOptions != null && columnOptions.stream() + .anyMatch(option -> option.getKind() == ColumnOption.Kind.SERIAL_DEFAULT_VALUE); + } + + public ForeignKeyReference getForeignKeyReference() { + if (columnOptions == null) { + return null; + } + return columnOptions.stream() + .filter(option -> option.getKind() == ColumnOption.Kind.REFERENCE) + .map(ColumnOption::getForeignKeyReference) + .findFirst() + .orElse(null); + } + + public ColumnDefinition withColumnOptions(List columnOptions) { + setColumnOptions(columnOptions); + return this; + } + + public ColumnDefinition addColumnOptions(ColumnOption... columnOptions) { + List collection = + Optional.ofNullable(getColumnOptions()).orElseGet(ArrayList::new); + Collections.addAll(collection, columnOptions); + return withColumnOptions(collection); } public ColDataType getColDataType() { @@ -71,9 +113,11 @@ public String toString() { public String toStringDataTypeAndSpec() { return (colDataType == null ? "" : colDataType) - + (columnSpecs != null && !columnSpecs.isEmpty() - ? " " + PlainSelect.getStringList(columnSpecs, false, false) - : ""); + + (columnOptions != null && !columnOptions.isEmpty() + ? " " + PlainSelect.getStringList(columnOptions, false, false) + : columnSpecs != null && !columnSpecs.isEmpty() + ? " " + PlainSelect.getStringList(columnSpecs, false, false) + : ""); } public ColumnDefinition withColumnName(String columnName) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java new file mode 100644 index 000000000..2a03678d1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java @@ -0,0 +1,70 @@ +/*- + * #%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.Arrays; +import java.util.Collections; +import java.util.List; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** A structured option following a column data type. */ +public class ColumnOption implements Serializable { + + public enum Kind { + SERIAL_DEFAULT_VALUE, REFERENCE, OTHER + } + + private Kind kind = Kind.OTHER; + private List tokens; + private ForeignKeyReference foreignKeyReference; + + public static ColumnOption raw(List tokens) { + ColumnOption option = new ColumnOption(); + option.tokens = tokens; + return option; + } + + public static ColumnOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public static ColumnOption serialDefaultValue() { + ColumnOption option = raw("SERIAL", "DEFAULT", "VALUE"); + option.kind = Kind.SERIAL_DEFAULT_VALUE; + return option; + } + + public static ColumnOption reference(ForeignKeyReference reference) { + ColumnOption option = new ColumnOption(); + option.kind = Kind.REFERENCE; + option.foreignKeyReference = reference; + return option; + } + + public Kind getKind() { + return kind; + } + + public List getTokens() { + return kind == Kind.REFERENCE ? Collections.singletonList(foreignKeyReference.toString()) + : tokens; + } + + public ForeignKeyReference getForeignKeyReference() { + return foreignKeyReference; + } + + @Override + public String toString() { + return kind == Kind.REFERENCE ? foreignKeyReference.toString() + : PlainSelect.getStringList(tokens, false, false); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index e5c8b6806..24b92511b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -28,9 +28,11 @@ public class CreateTable implements Statement { private boolean unlogged = false; private List createOptionsStrings; private List tableOptionsStrings; + private List tableOptions; private List columnDefinitions; private List columns; private List indexes; + private List tableElements; private Select select; private Table likeTable; private boolean selectParenthesis; @@ -74,6 +76,7 @@ public List getColumnDefinitions() { public void setColumnDefinitions(List list) { columnDefinitions = list; + tableElements = null; } public List getColumns() { @@ -94,6 +97,31 @@ public List getTableOptionsStrings() { public void setTableOptionsStrings(List tableOptionsStrings) { this.tableOptionsStrings = tableOptionsStrings; + tableOptions = null; + } + + /** Returns typed table options in source order. */ + public List getTableOptions() { + return tableOptions; + } + + public void setTableOptions(List tableOptions) { + this.tableOptions = tableOptions; + if (tableOptions == null) { + tableOptionsStrings = null; + return; + } + tableOptionsStrings = new ArrayList<>(); + for (TableOption option : tableOptions) { + tableOptionsStrings.addAll(option.getTokens()); + } + } + + /** Returns the first option of the requested kind, if present. */ + public Optional getTableOption(TableOption.Kind kind) { + return Optional.ofNullable(tableOptions).orElseGet(Collections::emptyList).stream() + .filter(option -> option.getKind() == kind) + .findFirst(); } public List getCreateOptionsStrings() { @@ -115,6 +143,46 @@ public List getIndexes() { public void setIndexes(List list) { indexes = list; + tableElements = null; + } + + /** + * Returns columns, constraints, and indexes in the order in which they were declared. + */ + public List getTableElements() { + return tableElements; + } + + public void setTableElements(List tableElements) { + this.tableElements = tableElements; + if (tableElements == null) { + columnDefinitions = null; + indexes = null; + return; + } + columnDefinitions = new ArrayList<>(); + indexes = new ArrayList<>(); + for (TableElement element : tableElements) { + if (element instanceof ColumnDefinition) { + columnDefinitions.add((ColumnDefinition) element); + } else if (element instanceof Index) { + indexes.add((Index) element); + } + } + } + + /** Returns table elements of a requested AST type while preserving their declaration order. */ + public List getTableElements(Class type) { + if (tableElements == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (TableElement element : tableElements) { + if (type.isInstance(element)) { + result.add(type.cast(element)); + } + } + return result; } public Select getSelect() { @@ -233,7 +301,11 @@ private void appendColumnDefinitions(StringBuilder b) { b.append(" "); b.append(PlainSelect.getStringList(columns, true, true)); } - if (columnDefinitions != null && !columnDefinitions.isEmpty()) { + if (tableElements != null && !tableElements.isEmpty()) { + b.append(" ("); + b.append(PlainSelect.getStringList(tableElements, true, false)); + b.append(")"); + } else if (columnDefinitions != null && !columnDefinitions.isEmpty()) { b.append(" ("); b.append(PlainSelect.getStringList(columnDefinitions, true, false)); if (indexes != null && !indexes.isEmpty()) { @@ -245,7 +317,9 @@ private void appendColumnDefinitions(StringBuilder b) { } private void appendTableOptions(StringBuilder b) { - String options = PlainSelect.getStringList(tableOptionsStrings, false, false); + String options = tableOptions != null + ? PlainSelect.getStringList(tableOptions, false, false) + : PlainSelect.getStringList(tableOptionsStrings, false, false); if (options != null && options.length() > 0) { b.append(" ").append(options); } @@ -333,6 +407,11 @@ public CreateTable withTableOptionsStrings(List tableOptionsStrings) { return this; } + public CreateTable withTableOptions(List tableOptions) { + this.setTableOptions(tableOptions); + return this; + } + public CreateTable withColumnDefinitions(List columnDefinitions) { this.setColumnDefinitions(columnDefinitions); return this; @@ -348,6 +427,11 @@ public CreateTable withIndexes(List indexes) { return this; } + public CreateTable withTableElements(List tableElements) { + this.setTableElements(tableElements); + return this; + } + public CreateTable addCreateOptionsStrings(String... createOptionsStrings) { List collection = Optional.ofNullable(getCreateOptionsStrings()).orElseGet(ArrayList::new); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java index 1ad10cb16..76df31a75 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java @@ -28,21 +28,58 @@ public class ForeignKeyIndex extends NamedConstraint { private Table table; private List referencedColumnNames; private Set referentialActions = new LinkedHashSet<>(2); + private ForeignKeyReference reference; + + public ForeignKeyReference getReference() { + if (reference == null) { + reference = new ForeignKeyReference(); + reference.setTable(table); + reference.setReferencedColumnNames(referencedColumnNames); + for (ReferentialAction action : referentialActions) { + reference.setReferentialAction(action.getType(), action.getAction()); + } + } + return reference; + } + + public void setReference(ForeignKeyReference reference) { + this.reference = reference; + if (reference != null) { + table = reference.getTable(); + referencedColumnNames = reference.getReferencedColumnNames(); + referentialActions.clear(); + referentialActions.addAll(reference.getReferentialActions()); + } + } + + public ForeignKeyReference.MatchType getMatchType() { + return reference != null ? reference.getMatchType() : null; + } + + public void setMatchType(ForeignKeyReference.MatchType matchType) { + getReference().setMatchType(matchType); + } public Table getTable() { - return table; + return reference != null ? reference.getTable() : table; } public void setTable(Table table) { this.table = table; + if (reference != null) { + reference.setTable(table); + } } public List getReferencedColumnNames() { - return referencedColumnNames; + return reference != null ? reference.getReferencedColumnNames() : referencedColumnNames; } public void setReferencedColumnNames(List referencedColumnNames) { this.referencedColumnNames = referencedColumnNames; + if (reference != null) { + reference.setReferencedColumnNames(referencedColumnNames); + } } /** @@ -70,6 +107,9 @@ public void removeReferentialAction(Type type) { * @return */ public ReferentialAction getReferentialAction(Type type) { + if (reference != null) { + return reference.getReferentialAction(type); + } return referentialActions.stream().filter(ra -> type.equals(ra.getType())).findFirst() .orElse(null); } @@ -77,6 +117,10 @@ public ReferentialAction getReferentialAction(Type type) { private void setReferentialAction(Type type, Action action, boolean set) { ReferentialAction found = getReferentialAction(type); if (set) { + if (reference != null) { + reference.setReferentialAction(type, action); + return; + } if (found == null) { referentialActions.add(new ReferentialAction(type, action)); } else { @@ -84,6 +128,9 @@ private void setReferentialAction(Type type, Action action, boolean set) { } } else if (found != null) { referentialActions.remove(found); + if (reference != null) { + reference.removeReferentialAction(type); + } } } @@ -119,9 +166,14 @@ public void setOnUpdateReferenceOption(String onUpdateReferenceOption) { @Override public String toString() { - StringBuilder b = new StringBuilder(super.toString()).append(" REFERENCES ").append(table) - .append(PlainSelect.getStringList(getReferencedColumnNames(), true, true)); - referentialActions.forEach(b::append); + StringBuilder b = new StringBuilder(super.toString()).append(" "); + if (reference != null) { + b.append(reference); + } else { + b.append("REFERENCES ").append(table) + .append(PlainSelect.getStringList(getReferencedColumnNames(), true, true)); + referentialActions.forEach(b::append); + } return b.toString(); } @@ -135,6 +187,16 @@ public ForeignKeyIndex withReferencedColumnNames(List referencedColumnNa return this; } + public ForeignKeyIndex withReference(ForeignKeyReference reference) { + setReference(reference); + return this; + } + + public ForeignKeyIndex withMatchType(ForeignKeyReference.MatchType matchType) { + setMatchType(matchType); + return this; + } + public ForeignKeyIndex withOnDeleteReferenceOption(String onDeleteReferenceOption) { this.setOnDeleteReferenceOption(onDeleteReferenceOption); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java new file mode 100644 index 000000000..607a9d0df --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyReference.java @@ -0,0 +1,135 @@ +/*- + * #%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.LinkedHashSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.ReferentialAction; +import net.sf.jsqlparser.statement.ReferentialAction.Action; +import net.sf.jsqlparser.statement.ReferentialAction.Type; +import net.sf.jsqlparser.statement.select.PlainSelect; + +/** The target and actions of a column- or table-level foreign-key reference. */ +public class ForeignKeyReference implements Serializable { + + public enum MatchType { + FULL, PARTIAL, SIMPLE + } + + private Table table; + private List referencedColumnNames; + private MatchType matchType; + private final Set referentialActions = new LinkedHashSet<>(2); + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public List getReferencedColumnNames() { + return referencedColumnNames; + } + + public void setReferencedColumnNames(List referencedColumnNames) { + this.referencedColumnNames = referencedColumnNames; + } + + public MatchType getMatchType() { + return matchType; + } + + public void setMatchType(MatchType matchType) { + this.matchType = matchType; + } + + public Set getReferentialActions() { + return referentialActions; + } + + public ReferentialAction getReferentialAction(Type type) { + return referentialActions.stream().filter(action -> type.equals(action.getType())) + .findFirst() + .orElse(null); + } + + public void setReferentialAction(Type type, Action action) { + ReferentialAction current = getReferentialAction(type); + if (current == null) { + referentialActions.add(new ReferentialAction(type, action)); + } else { + current.setAction(action); + } + } + + public void removeReferentialAction(Type type) { + ReferentialAction current = getReferentialAction(type); + if (current != null) { + referentialActions.remove(current); + } + } + + public ForeignKeyReference withTable(Table table) { + setTable(table); + return this; + } + + public ForeignKeyReference withReferencedColumnNames(List referencedColumnNames) { + setReferencedColumnNames(referencedColumnNames); + return this; + } + + public ForeignKeyReference withMatchType(MatchType matchType) { + setMatchType(matchType); + return this; + } + + public ForeignKeyReference withReferentialAction(Type type, Action action) { + setReferentialAction(type, action); + return this; + } + + public ForeignKeyReference addReferencedColumnNames(String... referencedColumnNames) { + List collection = Optional.ofNullable(getReferencedColumnNames()) + .orElseGet(ArrayList::new); + Collections.addAll(collection, referencedColumnNames); + return withReferencedColumnNames(collection); + } + + public ForeignKeyReference addReferencedColumnNames( + Collection referencedColumnNames) { + List collection = Optional.ofNullable(getReferencedColumnNames()) + .orElseGet(ArrayList::new); + collection.addAll(referencedColumnNames); + return withReferencedColumnNames(collection); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("REFERENCES ").append(table); + if (referencedColumnNames != null) { + builder.append(PlainSelect.getStringList(referencedColumnNames, true, true)); + } + if (matchType != null) { + builder.append(" MATCH ").append(matchType); + } + referentialActions.forEach(builder::append); + return builder.toString(); + } +} 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 9232f1dae..171f92baf 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 @@ -20,7 +20,11 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.select.PlainSelect; -public class Index implements Serializable { +public class Index implements TableElement, Serializable { + + public enum Kind { + PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, OTHER + } private final List name = new ArrayList<>(); private String type; @@ -29,10 +33,11 @@ public class Index implements Serializable { private List idxSpec; private String commentText; private String indexKeyword; + private Kind kind = Kind.OTHER; public List getColumnsNames() { return columns.stream() - .map(ColumnParams::getColumnName) + .map(ColumnParams::toString) .collect(toList()); } @@ -105,6 +110,34 @@ public String getType() { public void setType(String string) { type = string; + if (kind == Kind.OTHER && string != null) { + String normalized = string.toUpperCase(java.util.Locale.ROOT); + if (normalized.startsWith("PRIMARY")) { + kind = Kind.PRIMARY_KEY; + } else if (normalized.startsWith("UNIQUE")) { + kind = Kind.UNIQUE; + } else if (normalized.startsWith("FULLTEXT")) { + kind = Kind.FULLTEXT; + } else if (normalized.startsWith("SPATIAL")) { + kind = Kind.SPATIAL; + } else if (normalized.startsWith("FOREIGN")) { + kind = Kind.FOREIGN_KEY; + } else if (normalized.startsWith("CHECK")) { + kind = Kind.CHECK; + } else if (normalized.startsWith("EXCLUDE")) { + kind = Kind.EXCLUDE; + } else if (normalized.contains("INDEX") || normalized.contains("KEY")) { + kind = Kind.INDEX; + } + } + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; } public Index withColumnsNames(List list) { @@ -156,7 +189,11 @@ public Index withIndexKeyword(String indexKeyword) { @Override public String toString() { String idxSpecText = PlainSelect.getStringList(idxSpec, false, false); - String keyword = (indexKeyword != null) ? " " + indexKeyword : ""; + String keyword = indexKeyword != null + && (type == null || !type.toUpperCase(java.util.Locale.ROOT) + .endsWith(indexKeyword.toUpperCase(java.util.Locale.ROOT))) + ? " " + indexKeyword + : ""; String head = (type != null ? type : "") + keyword + @@ -176,6 +213,11 @@ public Index withType(String type) { return this; } + public Index withKind(Kind kind) { + setKind(kind); + return this; + } + public Index withUsing(String using) { this.setUsing(using); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java index 9746b26f2..ea11d0e08 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java @@ -47,7 +47,13 @@ public String toString() { String head = useConstraintKeyword || getName() != null ? "CONSTRAINT" + (getName() != null ? " " + getName() : "") + " " : ""; + String keyword = getIndexKeyword() != null + && !getType().toUpperCase(java.util.Locale.ROOT) + .endsWith(getIndexKeyword().toUpperCase(java.util.Locale.ROOT)) + ? " " + getIndexKeyword() + : ""; String tail = getType() + + keyword + (indexName != null ? " " + indexName : "") + (getUsing() != null ? " USING " + getUsing() : "") + " " + PlainSelect.getStringList(getColumnsNames(), true, true) + diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java new file mode 100644 index 000000000..ef4348a39 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableElement.java @@ -0,0 +1,22 @@ +/*- + * #%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; + +/** + * A column or table constraint/index declared inside a {@code CREATE TABLE} definition. + * + *

+ * This common type lets callers inspect table elements in their source order without merging the + * legacy column and index lists themselves. + */ +public interface TableElement extends Serializable { +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java new file mode 100644 index 000000000..dda98b276 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -0,0 +1,132 @@ +/*- + * #%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 net.sf.jsqlparser.statement.select.PlainSelect; + +/** A structured option following a {@code CREATE TABLE} definition. */ +public class TableOption implements Serializable { + + public enum Kind { + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, OTHER + } + + private Kind kind = Kind.OTHER; + private String name; + private String value; + private boolean useEquals; + private List tokens; + + public TableOption() {} + + public TableOption(Kind kind, String name, String value, boolean useEquals) { + this.kind = kind; + this.name = name; + this.value = value; + this.useEquals = useEquals; + } + + public static TableOption raw(List tokens) { + TableOption option = new TableOption(); + option.setTokens(tokens); + return option; + } + + public static TableOption raw(String... tokens) { + return raw(Arrays.asList(tokens)); + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + /** Returns the original token groups used by the legacy table-options API. */ + public List getTokens() { + if (tokens != null) { + return tokens; + } + List result = new ArrayList<>(); + if (name != null) { + Collections.addAll(result, name.trim().split("\\s+")); + } + if (useEquals) { + result.add("="); + } + if (value != null) { + result.add(value); + } + return Collections.unmodifiableList(result); + } + + public void setTokens(List tokens) { + this.tokens = tokens; + } + + public TableOption withKind(Kind kind) { + setKind(kind); + return this; + } + + public TableOption withName(String name) { + setName(name); + return this; + } + + public TableOption withValue(String value) { + setValue(value); + return this; + } + + public TableOption withUseEquals(boolean useEquals) { + setUseEquals(useEquals); + return this; + } + + @Override + public String toString() { + if (tokens != null) { + return PlainSelect.getStringList(tokens, false, false); + } + return name + (value != null ? (useEquals ? " = " : " ") + value : ""); + } +} From 3e67a33bae1241c2d7d24d63ffe50432f0c7700c Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 18:02:00 +0900 Subject: [PATCH 2/4] fix(parser): unify MySQL table definitions --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 664 +++++++++++------- .../create/MySqlTableDefinitionTest.java | 202 ++++++ 2 files changed, 631 insertions(+), 235 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 52994191b..d84900e45 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1191,6 +1191,60 @@ public class CCJSqlParser extends AbstractJSqlParser { return false; } + private boolean isKeywordAhead(String keyword) { + Token token = getToken(1); + return token.image != null && keyword.equalsIgnoreCase(token.image); + } + + private boolean isMySqlTableOptionAhead() { + int kind = getToken(1).kind; + if (kind == K_ENGINE) { + return true; + } + if (kind == K_CHARACTER && getToken(2).kind == K_SET + || kind == K_CHAR && getToken(2).kind == K_SET) { + return true; + } + if (kind == S_IDENTIFIER && "CHARSET".equalsIgnoreCase(getToken(1).image)) { + return true; + } + if (kind != K_DEFAULT) { + return false; + } + int secondKind = getToken(2).kind; + return secondKind == K_CHARACTER || secondKind == K_CHAR + || secondKind == S_IDENTIFIER + && "CHARSET".equalsIgnoreCase(getToken(2).image); + } + + private boolean isTableIndexAhead() { + switch (getToken(1).kind) { + case K_PRIMARY: + case K_UNIQUE: + case K_INDEX: + case K_KEY: + case K_FULLTEXT: + case K_SPATIAL: + return true; + default: + return false; + } + } + + private boolean isMySqlDialect() { + String dialect = getAsString(Feature.dialect); + return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect); + } + + private static boolean hasStructuredColumnOption(List options) { + for (ColumnOption option : options) { + if (option.getKind() != ColumnOption.Kind.OTHER) { + return true; + } + } + return false; + } + /** * Lightweight lookahead for SpecialStringFunctionWithNamedParameters: * scans forward from the current position (just past the opening '(') @@ -2215,7 +2269,7 @@ TOKEN: } } } -| < S_QUOTED_IDENTIFIER: "\"" ( "\"\"" | ~["\n","\r","\""])* "\"" | ("`" (~["\n","\r","`"])+ "`") | ( "[" (~["\n","\r","]"])* "]" ) > +| < S_QUOTED_IDENTIFIER: "\"" ( "\"\"" | ~["\n","\r","\""])* "\"" | ("`" ("``" | ~["\n","\r","`"])* "`") | ( "[" (~["\n","\r","]"])* "]" ) > { if ( !configuration.getAsBoolean(Feature.allowSquareBracketQuotation) && matchedToken.image.charAt(0) == '[' ) { @@ -4876,7 +4930,7 @@ String RelObjectName() : | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= ) ) @@ -11425,21 +11479,48 @@ ColumnDefinition ColumnDefinition(): { String columnName; ColDataType colDataType; List columnSpecs = new ArrayList(); - List parameter; + List columnOptions = new ArrayList(); + ColumnOption option; } { columnName=RelObjectName() colDataType=ColDataType() - ( LOOKAHEAD(2) parameter=ColumnDefinitionParameter() { columnSpecs.addAll(parameter); } )* + ( LOOKAHEAD(2) option=ColumnDefinitionOption() { + columnOptions.add(option); + columnSpecs.addAll(option.getTokens()); + } )* { coldef = new ColumnDefinition(); coldef.setColumnName(columnName); coldef.setColDataType(colDataType); if (columnSpecs.size() > 0) coldef.setColumnSpecs(columnSpecs); + if (hasStructuredColumnOption(columnOptions)) + coldef.setColumnOptions(columnOptions); return coldef; } } +ColumnOption ColumnDefinitionOption(): { + Token tk; + List parameter; + ForeignKeyReference reference; + ColumnOption option; +} { + ( + LOOKAHEAD({ isKeywordAhead("SERIAL") + && getToken(2).kind == K_DEFAULT && getToken(3).kind == K_VALUE }) + tk= + { option = ColumnOption.serialDefaultValue(); } + | + LOOKAHEAD() reference=ForeignKeyReferenceSpec() + { option = ColumnOption.reference(reference); } + | + parameter=ColumnDefinitionParameter() + { option = ColumnOption.raw(parameter); } + ) + { return option; } +} + CreateSchema CreateSchema(): { Token tk = null; @@ -11518,123 +11599,162 @@ CreateDatabase CreateDatabase(): } } +/** Parses PRIMARY, UNIQUE, plain, FULLTEXT, and SPATIAL indexes for CREATE and ALTER. */ +Index TableIndexSpec(boolean createContext): +{ + Token typeToken = null; + Token keywordToken = null; + String indexName = null; + String using = null; + List columns; + List indexOptions = new ArrayList(); + Index index; +} +{ + ( + typeToken= keywordToken= + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) + { + index = new NamedConstraint() + .withIndexName(indexName) + .withType(typeToken.image + " " + keywordToken.image) + .withColumns(columns) + .withIndexSpec(indexOptions); + } + | + typeToken= + [ LOOKAHEAD(2) (keywordToken= | keywordToken=) ] + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) + { + if (createContext) { + index = new NamedConstraint() + .withIndexName(indexName) + .withType(typeToken.image + + (keywordToken != null ? " " + keywordToken.image : "")) + .withUsing(using).withColumns(columns).withIndexSpec(indexOptions); + } else { + index = new Index().withType(typeToken.image) + .withIndexKeyword(keywordToken != null ? keywordToken.image : null) + .withName(indexName).withUsing(using).withColumns(columns) + .withIndexSpec(indexOptions); + } + } + | + typeToken= + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) + { + index = new Index().withType(createContext ? typeToken.image : null) + .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) + .withName(indexName).withUsing(using).withColumns(columns) + .withIndexSpec(indexOptions); + } + | + typeToken= + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET + && getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ using=UsingIndexType() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) + { + index = new Index().withType(createContext ? typeToken.image : null) + .withIndexKeyword(typeToken.image).withKind(Index.Kind.INDEX) + .withName(indexName).withUsing(using).withColumns(columns) + .withIndexSpec(indexOptions); + } + | + (typeToken= | typeToken=) + [ LOOKAHEAD(2) (keywordToken= | keywordToken=) ] + [ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ] + columns=IndexColumnsWithParamsList() + TableIndexOptions(createContext, indexOptions) + { + index = new Index().withType(typeToken.image + + (createContext && keywordToken != null + ? " " + keywordToken.image : "")) + .withIndexKeyword(!createContext && keywordToken != null + ? keywordToken.image : null) + .withName(indexName).withColumns(columns).withIndexSpec(indexOptions); + } + ) + { return index; } +} + +void TableIndexOptions(boolean createContext, List options): +{ + List parameter; +} +{ + ( + LOOKAHEAD({ createContext }) + ( LOOKAHEAD(2) parameter=CreateParameter() { options.addAll(parameter); } )* + | + LOOKAHEAD({ !createContext }) IndexOptionList(options) + ) +} + /** - * Parses a single table-level constraint inside CREATE TABLE (...). - * Handles INDEX, PRIMARY KEY, UNIQUE, KEY, FOREIGN KEY, CHECK, EXCLUDE. - * Returns an Index (which may be NamedConstraint, ForeignKeyIndex, CheckConstraint, ExcludeConstraint). + * Parses a single table-level constraint inside {@code CREATE TABLE (...)}. Index forms delegate + * to {@link #TableIndexSpec(boolean)} so CREATE and ALTER expose the same structured AST. */ Index CreateTableConstraint(): { Token tk = null; Token tk2 = null; - Token tk3 = null; - String sk3 = null; - String indexName = null; - String using = null; - boolean useConstraintKeyword = false; - List colNames = null; - List parameter = new ArrayList(); - List idxSpec = new ArrayList(); + String constraintName = null; Index index = null; ForeignKeyIndex fkIndex = null; - CheckConstraint checkCs = null; - ExcludeConstraint excludeC = null; - Expression exp = null; + CheckConstraint checkConstraint = null; + ExcludeConstraint excludeConstraint = null; + Expression expression = null; } { ( - LOOKAHEAD(4) ( - { idxSpec.clear(); tk3=null; } - [ tk3= ] - tk= - sk3=RelObjectName() - colNames = IndexColumnsWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* - { - index = new Index().withType((tk3!=null ? tk3.image + " " : "") + tk.image).withName(sk3).withColumns(colNames).withIndexSpec(new ArrayList(idxSpec)); - } - ) + LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(true) | - LOOKAHEAD(3) ( - { - index = new NamedConstraint(); - tk2=null; - indexName=null; - using=null; - idxSpec.clear(); - } - [ { ((NamedConstraint) index).setUseConstraintKeyword(true); } - [ LOOKAHEAD({ getToken(1).kind != K_PRIMARY && getToken(1).kind != K_UNIQUE }) - sk3=RelObjectName() {index.setName(sk3);} ] - ] - ( - tk= tk2= - | - tk= [ LOOKAHEAD(2) (tk2= | tk2=) ] - [ LOOKAHEAD(2, { getToken(1).kind != K_USING }) indexName=RelObjectName() ] - [ LOOKAHEAD(2) using=UsingIndexType() ] - ) - { - index.setType( tk.image + ( tk2!=null ? " " + tk2.image : "" )); - ((NamedConstraint) index).setIndexName(indexName); - index.setUsing(using); - tk2=null; + + [ LOOKAHEAD({ !isTableIndexAhead() && getToken(1).kind != K_FOREIGN + && getToken(1).kind != K_CHECK }) constraintName=RelObjectName() ] + ( + LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(true) { + if (index instanceof NamedConstraint) { + ((NamedConstraint) index).setUseConstraintKeyword(true); + index.setName(constraintName); + } } - colNames = ColumnNamesWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* - { - index.withColumns(colNames).withIndexSpec(new ArrayList(idxSpec)); + | + fkIndex=ForeignKeySpec(constraintName) { + fkIndex.setUseConstraintKeyword(true); + index = fkIndex; } - ) - | - LOOKAHEAD(3) ( - { - tk=null; - tk3=null; - idxSpec.clear(); - } - [ tk= ] - [ tk3= | tk3= ] tk2= - sk3=RelObjectName() - colNames = IndexColumnsWithParamsList() - ( parameter=CreateParameter() { idxSpec.addAll(parameter); } )* - { - index = new Index() - .withType( ( tk!=null ? tk.image + " " : "") + ( tk3!=null ? tk3.image + " ":"" ) + tk2.image) - .withName(sk3) - .withColumns(colNames) - .withIndexSpec(new ArrayList(idxSpec)); + | + checkConstraint=CheckConstraintSpec(constraintName) { + checkConstraint.setUseConstraintKeyword(true); + index = checkConstraint; } ) | - LOOKAHEAD(3) ( - { sk3=null; useConstraintKeyword=false; } - [ { useConstraintKeyword=true; } - [ LOOKAHEAD({ getToken(1).kind != K_FOREIGN }) sk3=RelObjectName() ] - ] - fkIndex = ForeignKeySpec(sk3) - { fkIndex.setUseConstraintKeyword(useConstraintKeyword); index = fkIndex; } - ) + fkIndex=ForeignKeySpec(null) { index = fkIndex; } | - LOOKAHEAD(3) ( - { sk3 = null; useConstraintKeyword=false; } - [ { useConstraintKeyword=true; } - [ LOOKAHEAD({ getToken(1).kind != K_CHECK }) sk3 = RelObjectName() ] - ] - checkCs = CheckConstraintSpec(sk3) - { checkCs.setUseConstraintKeyword(useConstraintKeyword); index = checkCs; } - ) + checkConstraint=CheckConstraintSpec(null) { index = checkConstraint; } | - LOOKAHEAD(2) ( - tk= { excludeC = new ExcludeConstraint(); } - (tk2= - ("(" exp = Expression() ")")* {excludeC.setExpression(exp);}) - { index = excludeC; } - ) + tk= { excludeConstraint = new ExcludeConstraint(); } + (tk2= ("(" expression=Expression() ")")*) { + excludeConstraint.setExpression(expression); + excludeConstraint.setKind(Index.Kind.EXCLUDE); + index = excludeConstraint; + } ) - { - return index; - } + { return index; } } @@ -11643,13 +11763,16 @@ CreateTable CreateTable(boolean isUsingOrReplace): CreateTable createTable = new CreateTable(); Table table = null; List columnDefinitions = new ArrayList(); + List tableElements = new ArrayList(); List tableOptions = new ArrayList(); + List typedTableOptions = new ArrayList(); List createOptions = new ArrayList(); Token tk = null; ColumnDefinition coldef = null; List indexes = new ArrayList(); Index index = null; List parameter = new ArrayList(); + TableOption tableOption = null; SpannerInterleaveIn interleaveIn = null; Select select = null; Table likeTable = null; @@ -11681,19 +11804,25 @@ CreateTable CreateTable(boolean isUsingOrReplace): | ( "(" - coldef = ColumnDefinition() { columnDefinitions.add(coldef); } + ( + LOOKAHEAD(3) index = CreateTableConstraint() + { indexes.add(index); tableElements.add(index); } + | + coldef = ColumnDefinition() + { columnDefinitions.add(coldef); tableElements.add(coldef); } + ) ( "," ( LOOKAHEAD(3) ( index = CreateTableConstraint() - { indexes.add(index); } + { indexes.add(index); tableElements.add(index); } ) | ( coldef = ColumnDefinition() - { columnDefinitions.add(coldef); } + { columnDefinitions.add(coldef); tableElements.add(coldef); } ) ) )* @@ -11707,7 +11836,19 @@ CreateTable CreateTable(boolean isUsingOrReplace): { createTable.setPartitionBound(partitionBound); } ] ( LOOKAHEAD(2, { getToken(1).kind != K_AS && !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) }) - parameter=CreateParameter() { tableOptions.addAll(parameter); } )* + ( + LOOKAHEAD({ isMySqlTableOptionAhead() }) + tableOption=MySqlTableOption() { + typedTableOptions.add(tableOption); + tableOptions.addAll(tableOption.getTokens()); + } + | + parameter=CreateParameter() { + typedTableOptions.add(TableOption.raw(parameter)); + tableOptions.addAll(parameter); + } + ) + )* [ partitioning=CreateTablePartitioning() { createTable.setPartitioning(partitioning); } ] // see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725 @@ -11721,20 +11862,69 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ interleaveIn = SpannerInterleaveIn( ) { createTable.setSpannerInterleaveIn(interleaveIn); } ] { createTable.setTable(table); - if (indexes.size() > 0) - createTable.setIndexes(indexes); + if (tableElements.size() > 0) + createTable.setTableElements(tableElements); if (createOptions.size() > 0) createTable.setCreateOptionsStrings(createOptions); - if (tableOptions.size() > 0) - createTable.setTableOptionsStrings(tableOptions); - if (columnDefinitions.size() > 0) - createTable.setColumnDefinitions(columnDefinitions); + if (typedTableOptions.size() > 0) + createTable.setTableOptions(typedTableOptions); if (columns.size() > 0) createTable.setColumns(columns); return createTable; } } +TableOption MySqlTableOption(): { + Token tk; + Token tk2 = null; + String value; + boolean useEquals = false; + String name = ""; + TableOption.Kind kind; + TableOption option; +} { + ( + tk= { + name = tk.image; + kind = TableOption.Kind.ENGINE; + } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + | + [ tk= { name = tk.image + " "; } ] + ( + tk= tk2= { + name += tk.image + " " + tk2.image; + } + | + tk= tk2= { + name += tk.image + " " + tk2.image; + } + | + tk= { name += tk.image; } + ) + { kind = TableOption.Kind.CHARACTER_SET; } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + ) + { + option = new TableOption(kind, name, value, useEquals); + return option; + } +} + +String MySqlTableOptionValue(): { + Token token; + String value = null; +} { + ( + value=RelObjectName() + | token= { value = token.image; } + | token= { value = token.image; } + ) + { return value; } +} + SpannerInterleaveIn SpannerInterleaveIn(): { Table table = null; @@ -11840,6 +12030,10 @@ ColDataType ColDataType(): int precision = -1; int scale = -1; + Token national = null; + Token nationalType = null; + Token varying = null; + ColDataType.TypeModifier typeModifier = null; } { ( @@ -11856,8 +12050,35 @@ ColDataType ColDataType(): ")" { colDataType = new ColDataType("STRUCT"); } ) | + LOOKAHEAD({ isKeywordAhead("NATIONAL") }) ( + national= + (nationalType= | nationalType= | nationalType=) + [ LOOKAHEAD({ isKeywordAhead("VARYING") }) varying= ] + { + type = national.image + " " + nationalType.image + + (varying != null ? " " + varying.image : ""); + colDataType.setDataType(type); + colDataType.setNationalCharacterType( + nationalType.image.equalsIgnoreCase("VARCHAR") || varying != null + ? ColDataType.NationalCharacterType.VARCHAR + : ColDataType.NationalCharacterType.CHAR); + } + ) + | LOOKAHEAD(2) ( colDataType = DataType() + { + if (isMySqlDialect() + && colDataType.getDataType().toUpperCase(Locale.ROOT).startsWith("NCHAR")) { + colDataType.setNationalCharacterType( + colDataType.getDataType().toUpperCase(Locale.ROOT).contains("VARCHAR") + ? ColDataType.NationalCharacterType.VARCHAR + : ColDataType.NationalCharacterType.CHAR); + } else if (isMySqlDialect() && colDataType.getDataType().toUpperCase(Locale.ROOT) + .startsWith("NVARCHAR")) { + colDataType.setNationalCharacterType(ColDataType.NationalCharacterType.VARCHAR); + } + } ) | ( @@ -11927,11 +12148,8 @@ ColDataType ColDataType(): )* ")" ] - [ LOOKAHEAD(2) - ( tk= { colDataType.setSignedness(ColDataType.Signedness.SIGNED); } - | tk= { colDataType.setSignedness(ColDataType.Signedness.UNSIGNED); } ) - ] - [ LOOKAHEAD(2) { colDataType.setZerofill(true); } ] + ( LOOKAHEAD(1) typeModifier=MySqlTypeModifier() + { colDataType.addTypeModifier(typeModifier); } )* [ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk= ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ] [ LOOKAHEAD(2) (tk= | tk=) { colDataType.setCharacterSet(tk.image); } ] @@ -11954,6 +12172,19 @@ ColDataType ColDataType(): } } +ColDataType.TypeModifier MySqlTypeModifier(): +{ + ColDataType.TypeModifier modifier; +} +{ + ( + { modifier = ColDataType.TypeModifier.SIGNED; } + | { modifier = ColDataType.TypeModifier.UNSIGNED; } + | { modifier = ColDataType.TypeModifier.ZEROFILL; } + ) + { return modifier; } +} + Analyze Analyze(): { Analyze analyze = new Analyze(); @@ -12064,7 +12295,7 @@ ReferentialAction.Action Action(): * Parses optional referential actions: [ON DELETE|UPDATE action] [ON DELETE|UPDATE action] * Shared between CREATE TABLE FK and ALTER TABLE FK definitions. */ -void ReferentialActionsOnIndex(ForeignKeyIndex fkIndex): +void ReferentialActions(ForeignKeyReference reference): { Token tk; ReferentialAction.Action action = null; @@ -12073,15 +12304,46 @@ void ReferentialActionsOnIndex(ForeignKeyIndex fkIndex): [ LOOKAHEAD(2) ( ( tk= | tk= ) action = Action() - { fkIndex.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } + { reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } )] [ LOOKAHEAD(2) ( ( tk= | tk= ) action = Action() - { fkIndex.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } + { reference.setReferentialAction(ReferentialAction.Type.from(tk.image), action); } )] } +ForeignKeyReference ForeignKeyReferenceSpec(): +{ + ForeignKeyReference reference = new ForeignKeyReference(); + ForeignKeyReference.MatchType matchType; + Token matchToken; + List refColNames = null; + Table fkTable; +} +{ + fkTable=Table() [ LOOKAHEAD(2) refColNames=ColumnsNamesList() ] + { + reference.setTable(fkTable); + reference.setReferencedColumnNames(refColNames); + } + [ + + ( + { matchType = ForeignKeyReference.MatchType.FULL; } + | matchToken= { + matchType = ForeignKeyReference.MatchType.valueOf( + matchToken.image.toUpperCase(Locale.ROOT)); + } + ) + { reference.setMatchType(matchType); } + ] + ReferentialActions(reference) + { + return reference; + } +} + /** * Parses: CHECK ( expression ) * Returns a CheckConstraint. Shared between CREATE TABLE and ALTER TABLE. @@ -12113,9 +12375,8 @@ ForeignKeyIndex ForeignKeySpec(String constraintName): String indexName = null; Token tk; Token tk2; - List refColNames = null; List colNames; - Table fkTable; + ForeignKeyReference reference; } { tk= tk2= @@ -12125,12 +12386,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName): if (constraintName != null) { fkIndex.setName(constraintName); } fkIndex.withType(tk.image + " " + tk2.image).withColumns(colNames); } - fkTable=Table() [ LOOKAHEAD(2) refColNames=ColumnsNamesList() ] - { - fkIndex.setTable(fkTable); - fkIndex.setReferencedColumnNames(refColNames); - } - ReferentialActionsOnIndex(fkIndex) + reference=ForeignKeyReferenceSpec() { fkIndex.setReference(reference); } { return fkIndex; } @@ -12528,16 +12784,25 @@ AlterExpression.ColumnDataType AlterExpressionColumnDataType(): String columnName = null; boolean withType = false; ColDataType dataType = null; - List columnSpecs = null; - List parameter = null; + List columnSpecs = new ArrayList(); + List columnOptions = new ArrayList(); + ColumnOption option = null; + AlterExpression.ColumnDataType result; } { - columnName = RelObjectName() { columnSpecs = new ArrayList(); } + columnName = RelObjectName() ( LOOKAHEAD(2) { withType = true; } )? ( LOOKAHEAD(2) dataType = ColDataType() )? - ( LOOKAHEAD(2) parameter = CreateParameter() { columnSpecs.addAll(parameter); } )* + ( LOOKAHEAD(2) option = ColumnDefinitionOption() { + columnOptions.add(option); + columnSpecs.addAll(option.getTokens()); + } )* { - return new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs); + result = new AlterExpression.ColumnDataType(columnName, withType, dataType, columnSpecs); + if (hasStructuredColumnOption(columnOptions)) { + result.setColumnOptions(columnOptions); + } + return result; } } @@ -13138,21 +13403,8 @@ void AlterExpressionAddConstraint(AlterExpression alterExp): | sk3=RelObjectName() ( - ( tk= tk2= - columnNames=ColumnsNamesList() - { - fkIndex = new ForeignKeyIndex() - .withName(sk3) - .withType(tk.image + " " + tk2.image) - .withColumnsNames(columnNames); - columnNames = null; - } - fkTable=Table() [ LOOKAHEAD(2) columnNames=ColumnsNamesList() ] - { - fkIndex.withTable(fkTable).withReferencedColumnNames(columnNames); - alterExp.setIndex(fkIndex); - } - ReferentialActionsOnIndex(fkIndex) + ( fkIndex=ForeignKeySpec(sk3) + { alterExp.setIndex(fkIndex); } constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } ) | @@ -13398,65 +13650,38 @@ AlterExpression AlterExpressionAddAlterModify(): ) ( - LOOKAHEAD(2) ( columnNames=ColumnsNamesList() { alterExp.setPkColumns(columnNames); }) - - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - [ - AlterExpressionUsingIndex(alterExp) - ] - | - LOOKAHEAD(2) ( - (tk= { alterExp.setUk(true); } | tk=) - ( - LOOKAHEAD(3) - sk3 = RelObjectName() - [ LOOKAHEAD(2) sk4 = UsingIndexType() ] - [ LOOKAHEAD(2) indexColumnNames = IndexColumnsWithParamsList() ] - | - [ LOOKAHEAD(2) sk4 = UsingIndexType() ] - [ LOOKAHEAD(2) indexColumnNames = IndexColumnsWithParamsList() ] - ) - IndexOptionList(indexSpec = new ArrayList()) - { - index = new Index() - .withIndexKeyword(tk.image) - .withName(sk3) - .withUsing(sk4) - .withColumns(indexColumnNames) - .withIndexSpec(indexSpec); - + LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ALTER + && getToken(1).kind == K_INDEX }) + tk= sk3=RelObjectName() IndexOptionList(indexSpec) { + index = new Index().withIndexKeyword(tk.image).withName(sk3) + .withKind(Index.Kind.INDEX).withIndexSpec(indexSpec); alterExp.setIndex(index); - } - constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } - ) + } | - LOOKAHEAD(4) - ( - ( tk= | tk= ) - [ LOOKAHEAD(2) ( tk2= | tk2= ) ] - ( - sk3 = RelObjectName() - columnNames = ColumnsNamesList() - | - columnNames = ColumnsNamesList() - ) - IndexOptionList(indexSpec = new ArrayList()) - { - String type = tk.image; - String keyword = tk2 != null ? tk2.image : null; - index = new Index() - .withType(type) - .withIndexKeyword(keyword) - .withColumnsNames(columnNames) - .withIndexSpec(indexSpec); - - if (sk3 != null) { - index.setName(sk3); - } - - alterExp.setIndex(index); - } - ) + LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) { + alterExp.setIndex(index); + if (index.getKind() == Index.Kind.PRIMARY_KEY) { + alterExp.setPkColumns(index.getColumnsNames()); + } else if (index.getKind() == Index.Kind.UNIQUE) { + alterExp.setUkColumns(index.getColumnsNames()); + alterExp.setUkName(index instanceof NamedConstraint + ? ((NamedConstraint) index).getIndexName() : index.getName()); + alterExp.setUk(index.getType().toUpperCase(Locale.ROOT).contains("KEY")); + alterExp.setUkTypeSpecified(index.getIndexKeyword() != null); + for (String option : new ArrayList(index.getIndexSpec())) { + if (option.toUpperCase(Locale.ROOT).startsWith("USING ")) { + alterExp.addParameters("USING"); + alterExp.addParameters(option.substring("USING ".length())); + index.getIndexSpec().remove(option); + } else if (option.toUpperCase(Locale.ROOT).startsWith("COMMENT ")) { + index.setCommentText(option.substring("COMMENT ".length())); + index.getIndexSpec().remove(option); + } + } + } + } + constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); } + [ AlterExpressionUsingIndex(alterExp) ] | LOOKAHEAD(2) ( sk3=RelObjectName() tk= { alterExp.withColumnName(sk3).withCommentText(tk.image); } @@ -13500,37 +13725,6 @@ AlterExpression AlterExpressionAddAlterModify(): | LOOKAHEAD(3) AlterExpressionColumnChanges(alterExp) | - ( - { index = new Index().withType("UNIQUE"); } - ( - ( - tk2= { alterExp.setUk(true); } - | tk2= { alterExp.setUk(false); } - ) - [ (tk= | tk=) { - sk3 = tk.image; - alterExp.setUkName(sk3); - } ] - | - (tk= | tk=) { - sk3 = tk.image; - alterExp.setUkTypeSpecified(false); - alterExp.setUkName(sk3); - } - )? - columnNames=ColumnsNamesList() { - alterExp.setUkColumns(columnNames); - index.withIndexKeyword(tk2 != null ? tk2.image : null) - .withName(sk3) - .withColumnsNames(columnNames); - alterExp.setIndex(index); - } - [ - AlterExpressionUsingIndex(alterExp) - ] - [ LOOKAHEAD(2) index = IndexWithComment(index) { alterExp.setIndex(index); } ] - ) - | // Standalone FK now uses ForeignKeyIndex, same as CONSTRAINT FK ( { ForeignKeyIndex fkIndex; ReferentialAction ra; } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java new file mode 100644 index 000000000..062b34a96 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java @@ -0,0 +1,202 @@ +/*- + * #%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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ForeignKeyReference; +import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.TableOption; +import org.junit.jupiter.api.Test; + +/** Regression tests for MySQL table definitions and their structured AST representation. */ +public class MySqlTableDefinitionTest { + + @Test + public void testConstraintBeforeColumnDefinition() throws JSQLParserException { + CreateTable table = + parseMySql("CREATE TABLE inventory (PRIMARY KEY (id), id INT NOT NULL)"); + + assertEquals(2, table.getTableElements().size()); + assertInstanceOf(Index.class, table.getTableElements().get(0)); + assertInstanceOf(ColumnDefinition.class, table.getTableElements().get(1)); + assertEquals("PRIMARY KEY", table.getIndexes().get(0).getType()); + assertReparse(table); + } + + @Test + public void testNamedPrimaryAndSpecializedIndexes() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE documents (id INT, body TEXT, data JSON, " + + "PRIMARY KEY pk_documents (id), FULLTEXT INDEX ft_body (body), " + + "INDEX ((CAST(data AS CHAR(30)))))"); + + NamedConstraint primary = (NamedConstraint) table.getIndexes().get(0); + assertEquals("pk_documents", primary.getIndexName()); + assertEquals(Index.Kind.PRIMARY_KEY, primary.getKind()); + assertEquals(Index.Kind.FULLTEXT, table.getIndexes().get(1).getKind()); + assertTrue(table.getIndexes().get(2).getColumns().get(0).isExpression()); + assertReparse(table); + } + + @Test + public void testFulltextAndSpatialIndexes() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE map_entry (body TEXT, coordinates POINT, " + + "FULLTEXT KEY ft_body (body), SPATIAL INDEX sp_coordinates (coordinates))"); + + assertEquals(Index.Kind.FULLTEXT, table.getIndexes().get(0).getKind()); + assertEquals("KEY", table.getIndexes().get(0).getType().split(" ")[1]); + assertEquals(Index.Kind.SPATIAL, table.getIndexes().get(1).getKind()); + assertReparse(table); + } + + @Test + public void testStructuredColumnOptions() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE child_record (id SMALLINT UNSIGNED " + + "SERIAL DEFAULT VALUE NOT NULL, parent_id INT REFERENCES parent_record(id) " + + "MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL)"); + + ColumnDefinition id = table.getColumnDefinitions().get(0); + assertTrue(id.isSerialDefaultValue()); + + ForeignKeyReference reference = table.getColumnDefinitions().get(1) + .getForeignKeyReference(); + assertNotNull(reference); + assertEquals("parent_record", reference.getTable().getName()); + assertEquals(Arrays.asList("id"), reference.getReferencedColumnNames()); + assertEquals(ForeignKeyReference.MatchType.FULL, reference.getMatchType()); + assertNotNull(reference.getReferentialAction( + net.sf.jsqlparser.statement.ReferentialAction.Type.UPDATE)); + assertReparse(table); + } + + @Test + public void testNationalCharacterTypesAndOrderedModifiers() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE type_samples (label NATIONAL CHARACTER " + + "VARYING(64), code NATIONAL CHAR(8), flags BIGINT ZEROFILL SIGNED UNSIGNED " + + "SIGNED ZEROFILL)"); + + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(0).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.CHAR, + table.getColumnDefinitions().get(1).getColDataType().getNationalCharacterType()); + assertEquals(Arrays.asList(ColDataType.TypeModifier.ZEROFILL, + ColDataType.TypeModifier.SIGNED, ColDataType.TypeModifier.UNSIGNED, + ColDataType.TypeModifier.SIGNED, ColDataType.TypeModifier.ZEROFILL), + table.getColumnDefinitions().get(2).getColDataType().getTypeModifiers()); + assertReparse(table); + } + + @Test + public void testNationalCharacterAliases() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE localized_text (short_name NCHAR(16), " + + "long_name NVARCHAR(255), legacy_name NCHAR VARCHAR(64))"); + + assertEquals(ColDataType.NationalCharacterType.CHAR, + table.getColumnDefinitions().get(0).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(1).getColDataType().getNationalCharacterType()); + assertEquals(ColDataType.NationalCharacterType.VARCHAR, + table.getColumnDefinitions().get(2).getColDataType().getNationalCharacterType()); + assertReparse(table); + } + + @Test + public void testStructuredMySqlTableOptionsAndKeywordColumn() throws JSQLParserException { + CreateTable table = + parseMySql("CREATE TABLE log_entry (Position BIGINT, File VARCHAR(255)) " + + "ENGINE=CSV DEFAULT CHAR SET=utf8mb4"); + + assertEquals("File", table.getColumnDefinitions().get(1).getColumnName()); + assertEquals(TableOption.Kind.ENGINE, table.getTableOptions().get(0).getKind()); + assertEquals("CSV", table.getTableOptions().get(0).getValue()); + assertEquals(TableOption.Kind.CHARACTER_SET, table.getTableOptions().get(1).getKind()); + assertEquals("utf8mb4", table.getTableOptions().get(1).getValue()); + assertReparse(table); + } + + @Test + public void testEscapedBacktickIdentifier() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE `odd``name` (`value``part` INT)"); + + assertEquals("`odd``name`", table.getTable().getName()); + assertEquals("`value``part`", table.getColumnDefinitions().get(0).getColumnName()); + assertReparse(table); + } + + @Test + public void testTableForeignKeyUsesStructuredReference() throws JSQLParserException { + CreateTable table = parseMySql("CREATE TABLE order_line (customer_id BIGINT, " + + "CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer(id) " + + "MATCH FULL ON DELETE CASCADE)"); + + ForeignKeyIndex foreignKey = (ForeignKeyIndex) table.getIndexes().get(0); + assertEquals(Index.Kind.FOREIGN_KEY, foreignKey.getKind()); + assertEquals("customer", foreignKey.getReference().getTable().getName()); + assertEquals(ForeignKeyReference.MatchType.FULL, foreignKey.getMatchType()); + assertReparse(table); + } + + @Test + public void testAlterColumnUsesStructuredOptions() throws JSQLParserException { + Alter alter = (Alter) CCJSqlParserUtil.parse("ALTER TABLE child_record ADD COLUMN " + + "parent_id INT REFERENCES parent_record(id) MATCH FULL ON DELETE CASCADE", + parser -> parser.withDialect(Dialect.MYSQL)); + + AlterExpression.ColumnDataType column = + alter.getAlterExpressions().get(0).getColDataTypeList().get(0); + assertEquals("parent_record", column.getForeignKeyReference().getTable().getName()); + assertEquals(ForeignKeyReference.MatchType.FULL, + column.getForeignKeyReference().getMatchType()); + CCJSqlParserUtil.parse(alter.toString(), parser -> parser.withDialect(Dialect.MYSQL)); + } + + @Test + public void testCreateAndAlterTableIndexParity() throws JSQLParserException { + CreateTable create = parseMySql("CREATE TABLE search_item (id INT, body TEXT, " + + "PRIMARY KEY pk_search (id), FULLTEXT INDEX ft_body (body))"); + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE search_item ADD PRIMARY KEY pk_search (id), " + + "ADD FULLTEXT INDEX ft_body (body)", + parser -> parser.withDialect(Dialect.MYSQL)); + + assertEquals(create.getIndexes().get(0).getClass(), + alter.getAlterExpressions().get(0).getIndex().getClass()); + assertEquals(create.getIndexes().get(0).getKind(), + alter.getAlterExpressions().get(0).getIndex().getKind()); + assertEquals(create.getIndexes().get(0).toString(), + alter.getAlterExpressions().get(0).getIndex().toString()); + assertEquals(create.getIndexes().get(1).toString(), + alter.getAlterExpressions().get(1).getIndex().toString()); + CCJSqlParserUtil.parse(alter.toString(), parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static CreateTable parseMySql(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, + parser -> parser.withDialect(Dialect.MYSQL)); + } + + private static void assertReparse(CreateTable table) throws JSQLParserException { + parseMySql(table.toString()); + } +} From db733998325195298c3aac495f555b5b7a7ea29c Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 18:04:37 +0900 Subject: [PATCH 3/4] fix(parser): model common MySQL table options --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 36 ++++++++++++++++--- .../create/MySqlTableDefinitionTest.java | 18 ++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d84900e45..1bfe5cbc9 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1198,7 +1198,8 @@ public class CCJSqlParser extends AbstractJSqlParser { private boolean isMySqlTableOptionAhead() { int kind = getToken(1).kind; - if (kind == K_ENGINE) { + if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT + || kind == K_AUTO_INCREMENT) { return true; } if (kind == K_CHARACTER && getToken(2).kind == K_SET @@ -1212,7 +1213,7 @@ public class CCJSqlParser extends AbstractJSqlParser { return false; } int secondKind = getToken(2).kind; - return secondKind == K_CHARACTER || secondKind == K_CHAR + return secondKind == K_CHARACTER || secondKind == K_CHAR || secondKind == K_COLLATE || secondKind == S_IDENTIFIER && "CHARSET".equalsIgnoreCase(getToken(2).image); } @@ -11880,7 +11881,7 @@ TableOption MySqlTableOption(): { String value; boolean useEquals = false; String name = ""; - TableOption.Kind kind; + TableOption.Kind kind = null; TableOption option; } { ( @@ -11902,8 +11903,27 @@ TableOption MySqlTableOption(): { } | tk= { name += tk.image; } + | + tk= { + name += tk.image; + kind = TableOption.Kind.COLLATE; + } ) - { kind = TableOption.Kind.CHARACTER_SET; } + { if (kind == null) { kind = TableOption.Kind.CHARACTER_SET; } } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + | + tk= { + name = tk.image; + kind = TableOption.Kind.COMMENT; + } + [ "=" { useEquals = true; } ] + value=MySqlTableOptionValue() + | + tk= { + name = tk.image; + kind = TableOption.Kind.AUTO_INCREMENT; + } [ "=" { useEquals = true; } ] value=MySqlTableOptionValue() ) @@ -11921,6 +11941,9 @@ String MySqlTableOptionValue(): { value=RelObjectName() | token= { value = token.image; } | token= { value = token.image; } + | token= { value = token.image; } + | token= { value = token.image; } + | token= { value = token.image; } ) { return value; } } @@ -12352,6 +12375,7 @@ CheckConstraint CheckConstraintSpec(String constraintName): { Expression exp = null; Boolean enforced = null; + CheckConstraint checkConstraint; } { ( LOOKAHEAD(2) "(" exp = Expression() ")" )* @@ -12360,8 +12384,10 @@ CheckConstraint CheckConstraintSpec(String constraintName): { if (enforced == null) { enforced = true; } } ] { - return new CheckConstraint().withName(constraintName).withExpression(exp) + checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp) .withEnforced(enforced); + checkConstraint.setKind(Index.Kind.CHECK); + return checkConstraint; } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java index 062b34a96..cc0b580d1 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlTableDefinitionTest.java @@ -133,6 +133,24 @@ public void testStructuredMySqlTableOptionsAndKeywordColumn() throws JSQLParserE assertEquals(TableOption.Kind.CHARACTER_SET, table.getTableOptions().get(1).getKind()); assertEquals("utf8mb4", table.getTableOptions().get(1).getValue()); assertReparse(table); + + CreateTable quotedEngine = parseMySql("CREATE TABLE archive_entry (id INT) " + + "ENGINE='InnoDB'"); + assertEquals("'InnoDB'", quotedEngine.getTableOption(TableOption.Kind.ENGINE) + .orElseThrow().getValue()); + assertReparse(quotedEngine); + + CreateTable commonOptions = parseMySql("CREATE TABLE table_options " + + "(id BIGINT AUTO_INCREMENT PRIMARY KEY) ENGINE=InnoDB " + + "DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=7 " + + "COMMENT='archive'"); + assertEquals("utf8mb4_bin", commonOptions.getTableOption(TableOption.Kind.COLLATE) + .orElseThrow().getValue()); + assertEquals("7", commonOptions.getTableOption(TableOption.Kind.AUTO_INCREMENT) + .orElseThrow().getValue()); + assertEquals("'archive'", commonOptions.getTableOption(TableOption.Kind.COMMENT) + .orElseThrow().getValue()); + assertReparse(commonOptions); } @Test From 69b6383aa5e91ceef1fd8779e6c8106a3cf156bc Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 18:11:02 +0900 Subject: [PATCH 4/4] fix(ast): initialize check constraint kind --- .../sf/jsqlparser/statement/create/table/CheckConstraint.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index c9ee2be53..d1cde6b3c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -23,6 +23,10 @@ public class CheckConstraint extends NamedConstraint { private Boolean enforced; + public CheckConstraint() { + setKind(Kind.CHECK); + } + public Table getTable() { return table; }