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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,7 @@ public static final class ColumnDataType extends ColumnDefinition
private final boolean withType;
private boolean useSetData;
private Expression usingExpression;
private ColumnPosition position;

public boolean isUseSetData() {
return useSetData;
Expand Down Expand Up @@ -1924,6 +1925,48 @@ public void setIdentityAlterations(List<IdentityAlteration> identityAlterations)
identityAlterations == null ? null : new ArrayList<>(identityAlterations);
}

/**
* Returns the MySQL FIRST/AFTER placement rendered after this definition, or null when the
* column keeps its position.
*/
public ColumnPosition getPosition() {
return position;
}

public void setPosition(ColumnPosition position) {
this.position = position;
}

public ColumnDataType withPosition(ColumnPosition position) {
setPosition(position);
return this;
}

/**
* Returns the legacy specification snapshot: the option tokens followed by the tokens of
* {@link #getPosition()}. Returns null only when both are unset.
*/
@Override
public List<String> getColumnSpecs() {
List<String> specs = super.getColumnSpecs();
if (position == null) {
return specs;
}
List<String> tokens = specs == null ? new ArrayList<>() : new ArrayList<>(specs);
tokens.addAll(position.getTokens());
return Collections.unmodifiableList(tokens);
}

/**
* Replaces every option and the position with opaque tokens, matching
* {@link #getColumnSpecs()}. Tokens are never parsed.
*/
@Override
public void setColumnSpecs(List<String> list) {
super.setColumnSpecs(list);
position = null;
}

public ColumnDataType(boolean withType) {
super();
this.withType = withType;
Expand Down Expand Up @@ -1958,6 +2001,9 @@ public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrint
builder.append(" USING ");
expressionPrinter.accept(usingExpression);
}
if (position != null) {
builder.append(' ').append(position);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*-
* #%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.alter;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* The MySQL/MariaDB {@code FIRST} or {@code AFTER column} placement that follows the column
* definition of an ALTER TABLE ADD, CHANGE or MODIFY action. Instances are immutable; replace the
* position of a definition to move the column.
*/
public final class ColumnPosition implements Serializable {

public enum Kind {
FIRST, AFTER
}

private static final ColumnPosition FIRST = new ColumnPosition(Kind.FIRST, null);

private final Kind kind;
private final String columnName;

private ColumnPosition(Kind kind, String columnName) {
this.kind = kind;
this.columnName = columnName;
}

/** Places the column at the start of the table. */
public static ColumnPosition first() {
return FIRST;
}

/** Places the column after the named existing column. */
public static ColumnPosition after(String columnName) {
return new ColumnPosition(Kind.AFTER, Objects.requireNonNull(columnName, "columnName"));
}

public Kind getKind() {
return kind;
}

/** Returns the preceding column of an AFTER position, or null for FIRST. */
public String getColumnName() {
return columnName;
}

/** Returns the SQL tokens of this position as an unmodifiable list. */
public List<String> getTokens() {
return kind == Kind.FIRST
? Collections.singletonList("FIRST")
: Collections.unmodifiableList(Arrays.asList("AFTER", columnName));
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ColumnPosition)) {
return false;
}
ColumnPosition position = (ColumnPosition) other;
return kind == position.kind && Objects.equals(columnName, position.columnName);
}

@Override
public int hashCode() {
return Objects.hash(kind, columnName);
}

@Override
public String toString() {
return kind == Kind.FIRST ? "FIRST" : "AFTER " + columnName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,31 @@
public class ColumnOption implements Serializable {

public enum Kind {
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, FOREIGN_OPTIONS, OTHER
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, FOREIGN_OPTIONS, SRID, COLUMN_FORMAT, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER
}

/** PostgreSQL storage strategies and MySQL column storage locations. */
public enum Storage {
PLAIN, EXTERNAL, EXTENDED, MAIN, DEFAULT, DISK, MEMORY
}

/** MySQL {@code COLUMN_FORMAT} values. */
public enum ColumnFormat {
FIXED, DYNAMIC, DEFAULT
}

/**
* Inline key spellings whose legacy tokens are split on spaces. KEY is MySQL's column-level
* synonym for PRIMARY KEY.
*/
private static final List<String> KEY_SPELLINGS =
Arrays.asList("PRIMARY KEY", "UNIQUE KEY", "KEY");

private List<ForeignDataOption> foreignOptions;
private Long srid;
private ColumnFormat columnFormat;
private StringValue engineAttribute;
private boolean useEquals;

public List<ForeignDataOption> getForeignOptions() {
return foreignOptions;
Expand Down Expand Up @@ -83,6 +99,77 @@ public void setCompression(String compression) {
this.compression = Objects.requireNonNull(compression, "compression");
}

/** Creates a MySQL {@code SRID} option for a spatial column. */
public static ColumnOption srid(long srid) {
ColumnOption option = new ColumnOption();
option.kind = Kind.SRID;
option.setSrid(srid);
return option;
}

/** Returns the spatial reference system identifier of an SRID option, or null. */
public Long getSrid() {
return srid;
}

public void setSrid(long srid) {
this.srid = srid;
}

/** Creates a MySQL {@code COLUMN_FORMAT} option. */
public static ColumnOption columnFormat(ColumnFormat columnFormat) {
ColumnOption option = new ColumnOption();
option.kind = Kind.COLUMN_FORMAT;
option.setColumnFormat(columnFormat);
return option;
}

public ColumnFormat getColumnFormat() {
return columnFormat;
}

public void setColumnFormat(ColumnFormat columnFormat) {
this.columnFormat = Objects.requireNonNull(columnFormat, "columnFormat");
}

/** Creates a MySQL {@code ENGINE_ATTRIBUTE} option with a JSON string literal. */
public static ColumnOption engineAttribute(StringValue attribute) {
ColumnOption option = new ColumnOption();
option.kind = Kind.ENGINE_ATTRIBUTE;
option.setEngineAttribute(attribute);
return option;
}

/** Creates a MySQL {@code SECONDARY_ENGINE_ATTRIBUTE} option with a JSON string literal. */
public static ColumnOption secondaryEngineAttribute(StringValue attribute) {
ColumnOption option = new ColumnOption();
option.kind = Kind.SECONDARY_ENGINE_ATTRIBUTE;
option.setEngineAttribute(attribute);
return option;
}

/** Returns the literal of an ENGINE_ATTRIBUTE or SECONDARY_ENGINE_ATTRIBUTE option, or null. */
public StringValue getEngineAttribute() {
return engineAttribute;
}

public void setEngineAttribute(StringValue attribute) {
engineAttribute = Objects.requireNonNull(attribute, "attribute");
}

/** Whether an engine attribute is written with the optional equals sign. */
public boolean isUseEquals() {
return useEquals;
}

public void setUseEquals(boolean useEquals) {
this.useEquals = useEquals;
}

private boolean isEngineAttribute() {
return kind == Kind.ENGINE_ATTRIBUTE || kind == Kind.SECONDARY_ENGINE_ATTRIBUTE;
}

private Kind kind = Kind.OTHER;
private List<String> tokens;
private ForeignKeyReference foreignKeyReference;
Expand Down Expand Up @@ -192,22 +279,23 @@ public void setGeneratedDefinition(GeneratedColumnDefinition definition) {
generatedDefinition = Objects.requireNonNull(definition, "definition");
}

/** Visits expressions of the selected option kind, including comment literals. */
/** Visits expressions of the selected option kind, including comment and attribute literals. */
public void visitExpressions(Consumer<Expression> visitor) {
if (kind == Kind.FOREIGN_OPTIONS) {
ForeignDataOption.visitExpressions(foreignOptions, visitor);
} else if (kind == Kind.DEFAULT) {
visitor.accept(defaultExpression);
} else if (kind == Kind.COMMENT) {
visitor.accept(comment);
} else if (isEngineAttribute()) {
visitor.accept(engineAttribute);
} else if (kind == Kind.ON_UPDATE) {
visitor.accept(onUpdateExpression);
} else if (kind == Kind.GENERATED) {
visitor.accept(generatedDefinition.getExpression());
}
}


/** Creates a DEFAULT option. Use a NullValue expression for SQL NULL. */
public static ColumnOption defaultValue(Expression expression) {
ColumnOption option = new ColumnOption();
Expand Down Expand Up @@ -321,10 +409,17 @@ public List<String> getTokens() {
case GENERATED:
return generatedDefinition.getTokens();
case CONSTRAINT:
if ("PRIMARY KEY".equals(constraint.toString())) {
return Collections.unmodifiableList(Arrays.asList("PRIMARY", "KEY"));
}
return Collections.singletonList(toString());
return constraintTokens();
case SRID:
return Collections.unmodifiableList(Arrays.asList("SRID", String.valueOf(srid)));
case COLUMN_FORMAT:
return Collections.unmodifiableList(
Arrays.asList("COLUMN_FORMAT", columnFormat.name()));
case ENGINE_ATTRIBUTE:
case SECONDARY_ENGINE_ATTRIBUTE:
return Collections.unmodifiableList(useEquals
? Arrays.asList(kind.name(), "=", engineAttribute.toString())
: Arrays.asList(kind.name(), engineAttribute.toString()));
case OTHER:
case SERIAL_DEFAULT_VALUE:
return tokens == null ? null
Expand All @@ -334,6 +429,14 @@ public List<String> getTokens() {
}
}

private List<String> constraintTokens() {
String sql = constraint.toString();
if (KEY_SPELLINGS.contains(sql)) {
return Collections.unmodifiableList(Arrays.asList(sql.split(" ")));
}
return Collections.singletonList(sql);
}

/**
* Replaces the tokens of an opaque {@link Kind#OTHER} option with a copy of the supplied list.
* Null unsets the tokens; an empty list retains an explicitly empty option. Tokens are never
Expand Down Expand Up @@ -374,6 +477,17 @@ public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrint
case COMPRESSION:
builder.append("COMPRESSION ").append(compression);
break;
case SRID:
builder.append("SRID ").append(srid);
break;
case COLUMN_FORMAT:
builder.append("COLUMN_FORMAT ").append(columnFormat);
break;
case ENGINE_ATTRIBUTE:
case SECONDARY_ENGINE_ATTRIBUTE:
builder.append(kind).append(useEquals ? " = " : " ");
expressionPrinter.accept(engineAttribute);
break;
case DEFAULT:
builder.append("DEFAULT ");
expressionPrinter.accept(defaultExpression);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
import static java.util.stream.Collectors.toList;

import java.util.EnumSet;
import java.util.List;
import net.sf.jsqlparser.parser.feature.Feature;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterExpression;
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDataType;
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull;
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull;
import net.sf.jsqlparser.statement.alter.AlterOperation;
import net.sf.jsqlparser.statement.alter.ColumnPosition;
import net.sf.jsqlparser.statement.create.table.NamedConstraint;
import net.sf.jsqlparser.statement.create.table.KeyColumnSource;
import net.sf.jsqlparser.statement.create.table.KeyElement;
Expand Down Expand Up @@ -71,6 +73,7 @@ public void validate(Alter alter, AlterExpression e) {
ValidationUtil.map(e.getColDataTypeList(), ColumnDataType::getColumnName),
validateForExist,
NamedObject.table);
validateOptionalColumnNames(c, precedingColumnNames(e), NamedObject.table);
}

validateOptionalName(c, NamedObject.constraint, e.getConstraintName());
Expand Down Expand Up @@ -148,4 +151,11 @@ private void validateIndex(ValidationCapability capability, AlterExpression expr
}
}

/** Returns the existing columns named by AFTER positions. */
private static List<String> precedingColumnNames(AlterExpression expression) {
return expression.getColDataTypeList().stream().map(ColumnDataType::getPosition)
.filter(position -> position != null
&& position.getKind() == ColumnPosition.Kind.AFTER)
.map(ColumnPosition::getColumnName).collect(toList());
}
}
Loading
Loading