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 6e18956e1..89f3d60b7 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 @@ -48,6 +48,17 @@ public enum CharacterSetSyntax { private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET; private IntervalQualifier intervalQualifier; private List arrayData = new ArrayList(); + 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; @@ -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); @@ -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) @@ -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); 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 index 23f2ef91f..c00dd7f63 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java @@ -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; @@ -256,6 +294,12 @@ public String toString() { /** Appends the option using the supplied printer for structured expressions. */ public void appendTo(StringBuilder builder, Consumer 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); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..bd7f83fda 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -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. @@ -14135,6 +14145,7 @@ ColumnOption ColumnDefinitionOption(): { GeneratedColumnDefinition generated; ObjectNames collationNames; String collation; + String compression; } { ( LOOKAHEAD({ isKeywordAhead("GENERATED") @@ -14161,6 +14172,12 @@ ColumnOption ColumnDefinitionOption(): { | LOOKAHEAD() { option = ColumnOption.autoIncrement(); } | + LOOKAHEAD() ( tk= | tk= | tk= ) + { option = ColumnOption.storage(accessEnum(ColumnOption.Storage.class, tk.image)); } + | + LOOKAHEAD({ isKeywordAhead("COMPRESSION") }) ContextualKeyword("COMPRESSION") compression=RelObjectName() + { option = ColumnOption.compression(compression); } + | LOOKAHEAD() ( tk= { collation = tk.image; } | collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } ) @@ -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); } ) @@ -15604,7 +15621,18 @@ ColDataType ColDataType(): ] ( 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() { tk = null; } + [ LOOKAHEAD(2) "[" tk= "]" ] { + array.add(tk == null ? null : Integer.valueOf(tk.image)); + colDataType.setArrayData(array); + colDataType.setUsingArrayKeyword(true); + } + | + [ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk= ] + { 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) diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java new file mode 100644 index 000000000..1ee7ca302 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnTypeOptionsTest.java @@ -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 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()); + } +}