Skip to content

Commit 8f6b3f2

Browse files
authored
feat: structure PostgreSQL array types and column storage options (#2704)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent e86fc13 commit 8f6b3f2

4 files changed

Lines changed: 202 additions & 6 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ public enum CharacterSetSyntax {
4848
private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
4949
private IntervalQualifier intervalQualifier;
5050
private List<Integer> arrayData = new ArrayList<Integer>();
51+
private boolean usingArrayKeyword;
52+
53+
/** ARRAY or ARRAY[n] spelling; arrayData still contains the single dimension. */
54+
public boolean isUsingArrayKeyword() {
55+
return usingArrayKeyword;
56+
}
57+
58+
public void setUsingArrayKeyword(boolean usingArrayKeyword) {
59+
this.usingArrayKeyword = usingArrayKeyword;
60+
}
61+
5162
private Signedness signedness;
5263
private boolean zerofill;
5364
private BigInteger precision;
@@ -365,7 +376,13 @@ public void setScale(Integer scale) {
365376
@Override
366377
public String toString() {
367378
StringBuilder arraySpec = new StringBuilder();
379+
if (usingArrayKeyword && !arrayData.isEmpty()) {
380+
arraySpec.append(" ARRAY");
381+
}
368382
for (Integer item : arrayData) {
383+
if (usingArrayKeyword && item == null) {
384+
continue;
385+
}
369386
arraySpec.append("[");
370387
if (item != null) {
371388
arraySpec.append(item);
@@ -498,6 +515,7 @@ public final boolean equals(Object o) {
498515
&& characterSetSyntax == that.characterSetSyntax
499516
&& Objects.equals(intervalQualifier, that.intervalQualifier)
500517
&& Objects.equals(arrayData, that.arrayData)
518+
&& usingArrayKeyword == that.usingArrayKeyword
501519
&& signedness == that.signedness
502520
&& zerofill == that.zerofill
503521
&& Objects.equals(typeModifiers, that.typeModifiers)
@@ -516,6 +534,7 @@ public int hashCode() {
516534
result = 31 * result + Objects.hashCode(characterSetSyntax);
517535
result = 31 * result + Objects.hashCode(intervalQualifier);
518536
result = 31 * result + Objects.hashCode(arrayData);
537+
result = 31 * result + Boolean.hashCode(usingArrayKeyword);
519538
result = 31 * result + Objects.hashCode(signedness);
520539
result = 31 * result + Boolean.hashCode(zerofill);
521540
result = 31 * result + Objects.hashCode(typeModifiers);

‎src/main/java/net/sf/jsqlparser/statement/create/table/ColumnOption.java‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,45 @@
2323
public class ColumnOption implements Serializable {
2424

2525
public enum Kind {
26-
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, OTHER
26+
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, STORAGE, COMPRESSION, OTHER
27+
}
28+
29+
/** PostgreSQL storage strategies and MySQL column storage locations. */
30+
public enum Storage {
31+
PLAIN, EXTERNAL, EXTENDED, MAIN, DEFAULT, DISK, MEMORY
32+
}
33+
34+
private Storage storage;
35+
private String compression;
36+
37+
public static ColumnOption storage(Storage storage) {
38+
ColumnOption option = new ColumnOption();
39+
option.kind = Kind.STORAGE;
40+
option.setStorage(storage);
41+
return option;
42+
}
43+
44+
public Storage getStorage() {
45+
return storage;
46+
}
47+
48+
public void setStorage(Storage storage) {
49+
this.storage = Objects.requireNonNull(storage, "storage");
50+
}
51+
52+
public static ColumnOption compression(String compression) {
53+
ColumnOption option = new ColumnOption();
54+
option.kind = Kind.COMPRESSION;
55+
option.setCompression(compression);
56+
return option;
57+
}
58+
59+
public String getCompression() {
60+
return compression;
61+
}
62+
63+
public void setCompression(String compression) {
64+
this.compression = Objects.requireNonNull(compression, "compression");
2765
}
2866

2967
private Kind kind = Kind.OTHER;
@@ -256,6 +294,12 @@ public String toString() {
256294
/** Appends the option using the supplied printer for structured expressions. */
257295
public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
258296
switch (kind) {
297+
case STORAGE:
298+
builder.append("STORAGE ").append(storage);
299+
break;
300+
case COMPRESSION:
301+
builder.append("COMPRESSION ").append(compression);
302+
break;
259303
case DEFAULT:
260304
builder.append("DEFAULT ");
261305
expressionPrinter.accept(defaultExpression);

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12672,7 +12672,17 @@ CastExpression CastExpression():
1267212672
)
1267312673
|
1267412674
(
12675-
type=ColDataType() { retval.setColDataType(type); }
12675+
type=ColDataType() {
12676+
// Preserve the established MySQL CAST type-name API outside PostgreSQL mode.
12677+
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
12678+
&& type.isUsingArrayKeyword() && type.getArrayData().size() == 1
12679+
&& type.getArrayData().get(0) == null) {
12680+
type.setUsingArrayKeyword(false);
12681+
type.getArrayData().clear();
12682+
type.setDataType(type.getDataType() + " ARRAY");
12683+
}
12684+
retval.setColDataType(type);
12685+
}
1267612686
// MySQL casts to an array of the given type when a multi-valued index key part is
1267712687
// defined, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). ARRAY reads as a further
1267812688
// word of the type, the way INT UNSIGNED already does.
@@ -14140,6 +14150,7 @@ ColumnOption ColumnDefinitionOption(): {
1414014150
GeneratedColumnDefinition generated;
1414114151
ObjectNames collationNames;
1414214152
String collation;
14153+
String compression;
1414314154
} {
1414414155
(
1414514156
LOOKAHEAD({ isKeywordAhead("GENERATED")
@@ -14166,6 +14177,12 @@ ColumnOption ColumnDefinitionOption(): {
1416614177
|
1416714178
LOOKAHEAD(<K_AUTO_INCREMENT>) <K_AUTO_INCREMENT> { option = ColumnOption.autoIncrement(); }
1416814179
|
14180+
LOOKAHEAD(<K_STORAGE>) <K_STORAGE> ( tk=<K_EXTENDED> | tk=<K_DEFAULT> | tk=<S_IDENTIFIER> )
14181+
{ option = ColumnOption.storage(accessEnum(ColumnOption.Storage.class, tk.image)); }
14182+
|
14183+
LOOKAHEAD({ isKeywordAhead("COMPRESSION") }) ContextualKeyword("COMPRESSION") compression=RelObjectName()
14184+
{ option = ColumnOption.compression(compression); }
14185+
|
1416914186
LOOKAHEAD(<K_COLLATE>) <K_COLLATE>
1417014187
( tk=<S_CHAR_LITERAL> { collation = tk.image; }
1417114188
| collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } )
@@ -15666,9 +15683,9 @@ ColDataType ColDataType():
1566615683
intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); }
1566715684
]
1566815685
[ LOOKAHEAD(2) "." arrayType = ColDataType() {
15669-
schema += "." + arrayType.toString();
15670-
colDataType.setPrecision(arrayType.getPrecision());
15671-
colDataType.setScale(arrayType.getScale());
15686+
schema += "." + arrayType.getDataType();
15687+
// Retain the nested type's structured suffixes instead of flattening them into its name.
15688+
colDataType = arrayType;
1567215689
} ]
1567315690
{ colDataType.setDataType(schema); }
1567415691
)
@@ -15711,7 +15728,18 @@ ColDataType ColDataType():
1571115728
]
1571215729
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
1571315730
{ colDataType.addTypeModifier(typeModifier); } )*
15714-
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
15731+
(
15732+
LOOKAHEAD(<K_ARRAY_LITERAL>) <K_ARRAY_LITERAL> { tk = null; }
15733+
[ LOOKAHEAD(2) "[" tk=<S_LONG> "]" ] {
15734+
array.add(tk == null ? null : Integer.valueOf(tk.image));
15735+
colDataType.setArrayData(array);
15736+
colDataType.setUsingArrayKeyword(true);
15737+
}
15738+
|
15739+
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ]
15740+
{ array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+
15741+
{ colDataType.setArrayData(array); } ]
15742+
)
1571515743
[
1571615744
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
1571715745
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.Arrays;
14+
import java.util.List;
15+
import net.sf.jsqlparser.JSQLParserException;
16+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
17+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
18+
import net.sf.jsqlparser.statement.Statement;
19+
import net.sf.jsqlparser.statement.alter.Alter;
20+
import net.sf.jsqlparser.statement.create.table.*;
21+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
26+
class PostgreSqlColumnTypeOptionsTest {
27+
@ParameterizedTest
28+
@ValueSource(strings = {"INT", "TEXT", "NUMERIC(10,2)", "TIMESTAMP(3) WITH TIME ZONE",
29+
"DOUBLE PRECISION", "public.mood"})
30+
void arrayKeywordBelongsToDataType(String type) throws JSQLParserException {
31+
for (boolean alter : new boolean[] {false, true}) {
32+
for (String dimensions : new String[] {"", "[4]"}) {
33+
Statement statement = parse(definition(alter, "a " + type + " ARRAY" + dimensions));
34+
ColumnDefinition column = column(statement);
35+
ColDataType dataType = column.getColDataType();
36+
assertTrue(dataType.isUsingArrayKeyword());
37+
assertEquals(dimensions.isEmpty() ? Arrays.asList((Integer) null) : List.of(4),
38+
dataType.getArrayData());
39+
assertNull(column.getColumnOptions());
40+
dataType.setArrayData(List.of(8));
41+
assertTrue(statement.toString().contains(" ARRAY[8]"));
42+
assertRoundTrip(statement);
43+
dataType.setUsingArrayKeyword(false);
44+
assertTrue(statement.toString().contains("[8]"));
45+
assertFalse(statement.toString().contains(" ARRAY"));
46+
assertRoundTrip(statement);
47+
}
48+
}
49+
}
50+
51+
@ParameterizedTest
52+
@ValueSource(strings = {"PLAIN", "EXTERNAL", "EXTENDED", "MAIN", "DEFAULT"})
53+
void storageAndCompressionAreSharedByCreateAndAddColumn(String storage)
54+
throws JSQLParserException {
55+
for (boolean alter : new boolean[] {false, true}) {
56+
Statement statement = parse(definition(alter,
57+
"a TEXT STORAGE " + storage + " COMPRESSION pglz COLLATE \"C\" DEFAULT 'x'"));
58+
List<ColumnOption> options = column(statement).getColumnOptions();
59+
assertEquals(ColumnOption.Storage.valueOf(storage), options.get(0).getStorage());
60+
assertEquals("pglz", options.get(1).getCompression());
61+
options.get(0).setStorage(ColumnOption.Storage.EXTENDED);
62+
options.get(1).setCompression("default");
63+
assertTrue(statement.toString().contains("STORAGE EXTENDED COMPRESSION default"));
64+
assertRoundTrip(statement);
65+
options.remove(0);
66+
assertFalse(statement.toString().contains("STORAGE"));
67+
assertRoundTrip(statement);
68+
}
69+
}
70+
71+
@Test
72+
void bracketArraysAndMysqlColumnStorageRemainValid() throws JSQLParserException {
73+
CreateTable table = (CreateTable) parse("CREATE TABLE t (a INT[3][4], b TEXT[])");
74+
assertEquals(List.of(3, 4),
75+
table.getColumnDefinitions().get(0).getColDataType().getArrayData());
76+
assertFalse(table.getColumnDefinitions().get(0).getColDataType().isUsingArrayKeyword());
77+
for (String value : new String[] {"DISK", "MEMORY"}) {
78+
Statement statement =
79+
CCJSqlParserUtil.parse("CREATE TABLE t (a INT STORAGE " + value + ")",
80+
p -> p.withDialect(Dialect.MYSQL));
81+
assertTrue(statement.toString().contains("STORAGE " + value));
82+
}
83+
}
84+
85+
private static String definition(boolean alter, String column) {
86+
return alter ? "ALTER TABLE t ADD COLUMN " + column : "CREATE TABLE t (" + column + ")";
87+
}
88+
89+
private static ColumnDefinition column(Statement statement) {
90+
return statement instanceof CreateTable
91+
? ((CreateTable) statement).getColumnDefinitions().get(0)
92+
: ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0);
93+
}
94+
95+
private static Statement parse(String sql) throws JSQLParserException {
96+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
97+
}
98+
99+
private static void assertRoundTrip(Statement statement) throws JSQLParserException {
100+
StringBuilder sql = new StringBuilder();
101+
statement.accept(new StatementDeParser(sql), null);
102+
assertEquals(statement.toString(), sql.toString());
103+
assertEquals(statement.toString(), parse(sql.toString()).toString());
104+
}
105+
}

0 commit comments

Comments
 (0)