Skip to content

Commit 6a4bdb6

Browse files
committed
feat: structure PostgreSQL array types and column storage options
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent dac8159 commit 6a4bdb6

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
@@ -12667,7 +12667,17 @@ CastExpression CastExpression():
1266712667
)
1266812668
|
1266912669
(
12670-
type=ColDataType() { retval.setColDataType(type); }
12670+
type=ColDataType() {
12671+
// Preserve the established MySQL CAST type-name API outside PostgreSQL mode.
12672+
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
12673+
&& type.isUsingArrayKeyword() && type.getArrayData().size() == 1
12674+
&& type.getArrayData().get(0) == null) {
12675+
type.setUsingArrayKeyword(false);
12676+
type.getArrayData().clear();
12677+
type.setDataType(type.getDataType() + " ARRAY");
12678+
}
12679+
retval.setColDataType(type);
12680+
}
1267112681
// MySQL casts to an array of the given type when a multi-valued index key part is
1267212682
// defined, e.g. CAST(data->'$.zips' AS UNSIGNED ARRAY). ARRAY reads as a further
1267312683
// word of the type, the way INT UNSIGNED already does.
@@ -14135,6 +14145,7 @@ ColumnOption ColumnDefinitionOption(): {
1413514145
GeneratedColumnDefinition generated;
1413614146
ObjectNames collationNames;
1413714147
String collation;
14148+
String compression;
1413814149
} {
1413914150
(
1414014151
LOOKAHEAD({ isKeywordAhead("GENERATED")
@@ -14161,6 +14172,12 @@ ColumnOption ColumnDefinitionOption(): {
1416114172
|
1416214173
LOOKAHEAD(<K_AUTO_INCREMENT>) <K_AUTO_INCREMENT> { option = ColumnOption.autoIncrement(); }
1416314174
|
14175+
LOOKAHEAD(<K_STORAGE>) <K_STORAGE> ( tk=<K_EXTENDED> | tk=<K_DEFAULT> | tk=<S_IDENTIFIER> )
14176+
{ option = ColumnOption.storage(accessEnum(ColumnOption.Storage.class, tk.image)); }
14177+
|
14178+
LOOKAHEAD({ isKeywordAhead("COMPRESSION") }) ContextualKeyword("COMPRESSION") compression=RelObjectName()
14179+
{ option = ColumnOption.compression(compression); }
14180+
|
1416414181
LOOKAHEAD(<K_COLLATE>) <K_COLLATE>
1416514182
( tk=<S_CHAR_LITERAL> { collation = tk.image; }
1416614183
| collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } )
@@ -15559,9 +15576,9 @@ ColDataType ColDataType():
1555915576
intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); }
1556015577
]
1556115578
[ LOOKAHEAD(2) "." arrayType = ColDataType() {
15562-
schema += "." + arrayType.toString();
15563-
colDataType.setPrecision(arrayType.getPrecision());
15564-
colDataType.setScale(arrayType.getScale());
15579+
schema += "." + arrayType.getDataType();
15580+
// Retain the nested type's structured suffixes instead of flattening them into its name.
15581+
colDataType = arrayType;
1556515582
} ]
1556615583
{ colDataType.setDataType(schema); }
1556715584
)
@@ -15604,7 +15621,18 @@ ColDataType ColDataType():
1560415621
]
1560515622
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
1560615623
{ colDataType.addTypeModifier(typeModifier); } )*
15607-
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
15624+
(
15625+
LOOKAHEAD(<K_ARRAY_LITERAL>) <K_ARRAY_LITERAL> { tk = null; }
15626+
[ LOOKAHEAD(2) "[" tk=<S_LONG> "]" ] {
15627+
array.add(tk == null ? null : Integer.valueOf(tk.image));
15628+
colDataType.setArrayData(array);
15629+
colDataType.setUsingArrayKeyword(true);
15630+
}
15631+
|
15632+
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ]
15633+
{ array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+
15634+
{ colDataType.setArrayData(array); } ]
15635+
)
1560815636
[
1560915637
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
1561015638
|| 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)