Skip to content
Merged
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 @@ -48,6 +48,17 @@ public enum CharacterSetSyntax {
private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
private IntervalQualifier intervalQualifier;
private List<Integer> arrayData = new ArrayList<Integer>();
private boolean usingArrayKeyword;

/** ARRAY or ARRAY[n] spelling; arrayData still contains the single dimension. */
public boolean isUsingArrayKeyword() {
return usingArrayKeyword;
}

public void setUsingArrayKeyword(boolean usingArrayKeyword) {
this.usingArrayKeyword = usingArrayKeyword;
}

private Signedness signedness;
private boolean zerofill;
private BigInteger precision;
Expand Down Expand Up @@ -365,7 +376,13 @@ public void setScale(Integer scale) {
@Override
public String toString() {
StringBuilder arraySpec = new StringBuilder();
if (usingArrayKeyword && !arrayData.isEmpty()) {
arraySpec.append(" ARRAY");
}
for (Integer item : arrayData) {
if (usingArrayKeyword && item == null) {
continue;
}
arraySpec.append("[");
if (item != null) {
arraySpec.append(item);
Expand Down Expand Up @@ -498,6 +515,7 @@ public final boolean equals(Object o) {
&& characterSetSyntax == that.characterSetSyntax
&& Objects.equals(intervalQualifier, that.intervalQualifier)
&& Objects.equals(arrayData, that.arrayData)
&& usingArrayKeyword == that.usingArrayKeyword
&& signedness == that.signedness
&& zerofill == that.zerofill
&& Objects.equals(typeModifiers, that.typeModifiers)
Expand All @@ -516,6 +534,7 @@ public int hashCode() {
result = 31 * result + Objects.hashCode(characterSetSyntax);
result = 31 * result + Objects.hashCode(intervalQualifier);
result = 31 * result + Objects.hashCode(arrayData);
result = 31 * result + Boolean.hashCode(usingArrayKeyword);
result = 31 * result + Objects.hashCode(signedness);
result = 31 * result + Boolean.hashCode(zerofill);
result = 31 * result + Objects.hashCode(typeModifiers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,45 @@
public class ColumnOption implements Serializable {

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

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

private Storage storage;
private String compression;

public static ColumnOption storage(Storage storage) {
ColumnOption option = new ColumnOption();
option.kind = Kind.STORAGE;
option.setStorage(storage);
return option;
}

public Storage getStorage() {
return storage;
}

public void setStorage(Storage storage) {
this.storage = Objects.requireNonNull(storage, "storage");
}

public static ColumnOption compression(String compression) {
ColumnOption option = new ColumnOption();
option.kind = Kind.COMPRESSION;
option.setCompression(compression);
return option;
}

public String getCompression() {
return compression;
}

public void setCompression(String compression) {
this.compression = Objects.requireNonNull(compression, "compression");
}

private Kind kind = Kind.OTHER;
Expand Down Expand Up @@ -256,6 +294,12 @@ public String toString() {
/** Appends the option using the supplied printer for structured expressions. */
public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
switch (kind) {
case STORAGE:
builder.append("STORAGE ").append(storage);
break;
case COMPRESSION:
builder.append("COMPRESSION ").append(compression);
break;
case DEFAULT:
builder.append("DEFAULT ");
expressionPrinter.accept(defaultExpression);
Expand Down
38 changes: 33 additions & 5 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -12667,7 +12667,17 @@ CastExpression CastExpression():
)
|
(
type=ColDataType() { retval.setColDataType(type); }
type=ColDataType() {
// Preserve the established MySQL CAST type-name API outside PostgreSQL mode.
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
&& type.isUsingArrayKeyword() && type.getArrayData().size() == 1
&& type.getArrayData().get(0) == null) {
type.setUsingArrayKeyword(false);
type.getArrayData().clear();
type.setDataType(type.getDataType() + " ARRAY");
}
retval.setColDataType(type);
}
// MySQL casts to an array of the given type when a multi-valued index key part is
// defined, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). ARRAY reads as a further
// word of the type, the way INT UNSIGNED already does.
Expand Down Expand Up @@ -14135,6 +14145,7 @@ ColumnOption ColumnDefinitionOption(): {
GeneratedColumnDefinition generated;
ObjectNames collationNames;
String collation;
String compression;
} {
(
LOOKAHEAD({ isKeywordAhead("GENERATED")
Expand All @@ -14161,6 +14172,12 @@ ColumnOption ColumnDefinitionOption(): {
|
LOOKAHEAD(<K_AUTO_INCREMENT>) <K_AUTO_INCREMENT> { option = ColumnOption.autoIncrement(); }
|
LOOKAHEAD(<K_STORAGE>) <K_STORAGE> ( tk=<K_EXTENDED> | tk=<K_DEFAULT> | tk=<S_IDENTIFIER> )
{ option = ColumnOption.storage(accessEnum(ColumnOption.Storage.class, tk.image)); }
|
LOOKAHEAD({ isKeywordAhead("COMPRESSION") }) ContextualKeyword("COMPRESSION") compression=RelObjectName()
{ option = ColumnOption.compression(compression); }
|
LOOKAHEAD(<K_COLLATE>) <K_COLLATE>
( tk=<S_CHAR_LITERAL> { collation = tk.image; }
| collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } )
Expand Down Expand Up @@ -15559,9 +15576,9 @@ ColDataType ColDataType():
intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); }
]
[ LOOKAHEAD(2) "." arrayType = ColDataType() {
schema += "." + arrayType.toString();
colDataType.setPrecision(arrayType.getPrecision());
colDataType.setScale(arrayType.getScale());
schema += "." + arrayType.getDataType();
// Retain the nested type's structured suffixes instead of flattening them into its name.
colDataType = arrayType;
} ]
{ colDataType.setDataType(schema); }
)
Expand Down Expand Up @@ -15604,7 +15621,18 @@ ColDataType ColDataType():
]
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
{ colDataType.addTypeModifier(typeModifier); } )*
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
(
LOOKAHEAD(<K_ARRAY_LITERAL>) <K_ARRAY_LITERAL> { tk = null; }
[ LOOKAHEAD(2) "[" tk=<S_LONG> "]" ] {
array.add(tk == null ? null : Integer.valueOf(tk.image));
colDataType.setArrayData(array);
colDataType.setUsingArrayKeyword(true);
}
|
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ]
{ array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+
{ colDataType.setArrayData(array); } ]
)
[
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*-
* #%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.*;
import java.util.Arrays;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.create.table.*;
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.ValueSource;

class PostgreSqlColumnTypeOptionsTest {
@ParameterizedTest
@ValueSource(strings = {"INT", "TEXT", "NUMERIC(10,2)", "TIMESTAMP(3) WITH TIME ZONE",
"DOUBLE PRECISION", "public.mood"})
void arrayKeywordBelongsToDataType(String type) throws JSQLParserException {
for (boolean alter : new boolean[] {false, true}) {
for (String dimensions : new String[] {"", "[4]"}) {
Statement statement = parse(definition(alter, "a " + type + " ARRAY" + dimensions));
ColumnDefinition column = column(statement);
ColDataType dataType = column.getColDataType();
assertTrue(dataType.isUsingArrayKeyword());
assertEquals(dimensions.isEmpty() ? Arrays.asList((Integer) null) : List.of(4),
dataType.getArrayData());
assertNull(column.getColumnOptions());
dataType.setArrayData(List.of(8));
assertTrue(statement.toString().contains(" ARRAY[8]"));
assertRoundTrip(statement);
dataType.setUsingArrayKeyword(false);
assertTrue(statement.toString().contains("[8]"));
assertFalse(statement.toString().contains(" ARRAY"));
assertRoundTrip(statement);
}
}
}

@ParameterizedTest
@ValueSource(strings = {"PLAIN", "EXTERNAL", "EXTENDED", "MAIN", "DEFAULT"})
void storageAndCompressionAreSharedByCreateAndAddColumn(String storage)
throws JSQLParserException {
for (boolean alter : new boolean[] {false, true}) {
Statement statement = parse(definition(alter,
"a TEXT STORAGE " + storage + " COMPRESSION pglz COLLATE \"C\" DEFAULT 'x'"));
List<ColumnOption> options = column(statement).getColumnOptions();
assertEquals(ColumnOption.Storage.valueOf(storage), options.get(0).getStorage());
assertEquals("pglz", options.get(1).getCompression());
options.get(0).setStorage(ColumnOption.Storage.EXTENDED);
options.get(1).setCompression("default");
assertTrue(statement.toString().contains("STORAGE EXTENDED COMPRESSION default"));
assertRoundTrip(statement);
options.remove(0);
assertFalse(statement.toString().contains("STORAGE"));
assertRoundTrip(statement);
}
}

@Test
void bracketArraysAndMysqlColumnStorageRemainValid() throws JSQLParserException {
CreateTable table = (CreateTable) parse("CREATE TABLE t (a INT[3][4], b TEXT[])");
assertEquals(List.of(3, 4),
table.getColumnDefinitions().get(0).getColDataType().getArrayData());
assertFalse(table.getColumnDefinitions().get(0).getColDataType().isUsingArrayKeyword());
for (String value : new String[] {"DISK", "MEMORY"}) {
Statement statement =
CCJSqlParserUtil.parse("CREATE TABLE t (a INT STORAGE " + value + ")",
p -> p.withDialect(Dialect.MYSQL));
assertTrue(statement.toString().contains("STORAGE " + value));
}
}

private static String definition(boolean alter, String column) {
return alter ? "ALTER TABLE t ADD COLUMN " + column : "CREATE TABLE t (" + column + ")";
}

private static ColumnDefinition column(Statement statement) {
return statement instanceof CreateTable
? ((CreateTable) statement).getColumnDefinitions().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0);
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}

private static void assertRoundTrip(Statement statement) throws JSQLParserException {
StringBuilder sql = new StringBuilder();
statement.accept(new StatementDeParser(sql), null);
assertEquals(statement.toString(), sql.toString());
assertEquals(statement.toString(), parse(sql.toString()).toString());
}
}
Loading