Skip to content

Commit 0e9c21a

Browse files
authored
feat: structure column attributes and generated expressions (#2641)
1 parent 5796619 commit 0e9c21a

10 files changed

Lines changed: 590 additions & 14 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ public void setColumnSpecs(List<String> list) {
7575
}
7676

7777
/**
78-
* Returns column options in source order, including structured defaults, references and MySQL
79-
* {@code SERIAL DEFAULT VALUE}.
78+
* Returns column options in source order, including defaults, references, generated columns,
79+
* nullability, collation, comments, visibility and MySQL attributes. Unrecognized options
80+
* remain raw. Structured option keywords use canonical capitalization when rendered.
8081
*/
8182
public List<ColumnOption> getColumnOptions() {
8283
return columnOptions;

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

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
import java.util.Objects;
1717
import java.util.function.Consumer;
1818
import net.sf.jsqlparser.expression.Expression;
19+
import net.sf.jsqlparser.expression.StringValue;
1920
import net.sf.jsqlparser.statement.select.PlainSelect;
2021

2122
/** A structured option following a column data type. */
2223
public class ColumnOption implements Serializable {
2324

2425
public enum Kind {
25-
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, OTHER
26+
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, NULLABILITY, COLLATE, COMMENT, ON_UPDATE, GENERATED, AUTO_INCREMENT, VISIBILITY, OTHER
2627
}
2728

2829
private Kind kind = Kind.OTHER;
@@ -31,6 +32,122 @@ public enum Kind {
3132
private IdentityDefinition identityDefinition;
3233
private Index constraint;
3334
private Expression defaultExpression;
35+
private Boolean nullable;
36+
private Boolean visible;
37+
private String collation;
38+
private StringValue comment;
39+
private Expression onUpdateExpression;
40+
private GeneratedColumnDefinition generatedDefinition;
41+
42+
public static ColumnOption nullability(boolean nullable) {
43+
ColumnOption option = new ColumnOption();
44+
option.kind = Kind.NULLABILITY;
45+
option.nullable = nullable;
46+
return option;
47+
}
48+
49+
public Boolean getNullable() {
50+
return nullable;
51+
}
52+
53+
public void setNullable(boolean nullable) {
54+
this.nullable = nullable;
55+
}
56+
57+
public static ColumnOption visibility(boolean visible) {
58+
ColumnOption option = new ColumnOption();
59+
option.kind = Kind.VISIBILITY;
60+
option.visible = visible;
61+
return option;
62+
}
63+
64+
public Boolean getVisible() {
65+
return visible;
66+
}
67+
68+
public void setVisible(boolean visible) {
69+
this.visible = visible;
70+
}
71+
72+
public static ColumnOption autoIncrement() {
73+
ColumnOption option = new ColumnOption();
74+
option.kind = Kind.AUTO_INCREMENT;
75+
return option;
76+
}
77+
78+
public static ColumnOption collate(String collation) {
79+
ColumnOption option = new ColumnOption();
80+
option.kind = Kind.COLLATE;
81+
option.setCollation(collation);
82+
return option;
83+
}
84+
85+
public String getCollation() {
86+
return collation;
87+
}
88+
89+
public void setCollation(String collation) {
90+
this.collation = Objects.requireNonNull(collation, "collation");
91+
}
92+
93+
public static ColumnOption comment(StringValue comment) {
94+
ColumnOption option = new ColumnOption();
95+
option.kind = Kind.COMMENT;
96+
option.setComment(comment);
97+
return option;
98+
}
99+
100+
public StringValue getComment() {
101+
return comment;
102+
}
103+
104+
public void setComment(StringValue comment) {
105+
this.comment = Objects.requireNonNull(comment, "comment");
106+
}
107+
108+
public static ColumnOption onUpdate(Expression expression) {
109+
ColumnOption option = new ColumnOption();
110+
option.kind = Kind.ON_UPDATE;
111+
option.setOnUpdateExpression(expression);
112+
return option;
113+
}
114+
115+
public Expression getOnUpdateExpression() {
116+
return onUpdateExpression;
117+
}
118+
119+
public void setOnUpdateExpression(Expression expression) {
120+
onUpdateExpression = Objects.requireNonNull(expression, "expression");
121+
}
122+
123+
public static ColumnOption generated(GeneratedColumnDefinition definition) {
124+
ColumnOption option = new ColumnOption();
125+
option.kind = Kind.GENERATED;
126+
option.setGeneratedDefinition(definition);
127+
return option;
128+
}
129+
130+
public GeneratedColumnDefinition getGeneratedDefinition() {
131+
return generatedDefinition;
132+
}
133+
134+
public void setGeneratedDefinition(GeneratedColumnDefinition definition) {
135+
generatedDefinition = Objects.requireNonNull(definition, "definition");
136+
}
137+
138+
/** Visits expressions of the selected option kind, including comment literals. */
139+
public void visitExpressions(Consumer<Expression> visitor) {
140+
if (kind == Kind.DEFAULT) {
141+
visitor.accept(defaultExpression);
142+
} else if (kind == Kind.COMMENT) {
143+
visitor.accept(comment);
144+
} else if (kind == Kind.ON_UPDATE) {
145+
visitor.accept(onUpdateExpression);
146+
} else if (kind == Kind.GENERATED) {
147+
visitor.accept(generatedDefinition.getExpression());
148+
}
149+
}
150+
34151

35152
/** Creates a DEFAULT option. Use a NullValue expression for SQL NULL. */
36153
public static ColumnOption defaultValue(Expression expression) {
@@ -99,11 +216,30 @@ public Kind getKind() {
99216
}
100217

101218
public List<String> getTokens() {
102-
if (kind == Kind.DEFAULT) {
103-
return Arrays.asList("DEFAULT", String.valueOf(defaultExpression));
219+
switch (kind) {
220+
case DEFAULT:
221+
return Arrays.asList("DEFAULT", String.valueOf(defaultExpression));
222+
case NULLABILITY:
223+
return nullable ? Collections.singletonList("NULL") : Arrays.asList("NOT", "NULL");
224+
case COLLATE:
225+
return Arrays.asList("COLLATE", collation);
226+
case COMMENT:
227+
return Arrays.asList("COMMENT", comment.toString());
228+
case ON_UPDATE:
229+
return Arrays.asList("ON", "UPDATE", onUpdateExpression.toString());
230+
case GENERATED:
231+
return generatedDefinition.getTokens();
232+
case CONSTRAINT:
233+
if ("PRIMARY KEY".equals(constraint.toString())) {
234+
return Arrays.asList("PRIMARY", "KEY");
235+
}
236+
return Collections.singletonList(toString());
237+
case OTHER:
238+
case SERIAL_DEFAULT_VALUE:
239+
return tokens;
240+
default:
241+
return Collections.singletonList(toString());
104242
}
105-
return kind == Kind.OTHER || kind == Kind.SERIAL_DEFAULT_VALUE ? tokens
106-
: Collections.singletonList(toString());
107243
}
108244

109245
public ForeignKeyReference getForeignKeyReference() {
@@ -124,6 +260,29 @@ public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrint
124260
builder.append("DEFAULT ");
125261
expressionPrinter.accept(defaultExpression);
126262
break;
263+
case NULLABILITY:
264+
builder.append(nullable ? "NULL" : "NOT NULL");
265+
break;
266+
case VISIBILITY:
267+
builder.append(visible ? "VISIBLE" : "INVISIBLE");
268+
break;
269+
case AUTO_INCREMENT:
270+
builder.append("AUTO_INCREMENT");
271+
break;
272+
case COLLATE:
273+
builder.append("COLLATE ").append(collation);
274+
break;
275+
case COMMENT:
276+
builder.append("COMMENT ");
277+
expressionPrinter.accept(comment);
278+
break;
279+
case ON_UPDATE:
280+
builder.append("ON UPDATE ");
281+
expressionPrinter.accept(onUpdateExpression);
282+
break;
283+
case GENERATED:
284+
generatedDefinition.appendTo(builder, expressionPrinter);
285+
break;
127286
case REFERENCE:
128287
builder.append(foreignKeyReference);
129288
break;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.table;
11+
12+
import java.io.Serializable;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Objects;
16+
import java.util.function.Consumer;
17+
import net.sf.jsqlparser.expression.Expression;
18+
19+
/** A parenthesized generated column expression, distinct from an identity declaration. */
20+
public class GeneratedColumnDefinition implements Serializable {
21+
public enum Storage {
22+
STORED, VIRTUAL
23+
}
24+
25+
private Expression expression;
26+
private boolean generatedAlways;
27+
private Storage storage;
28+
29+
public GeneratedColumnDefinition(Expression expression) {
30+
setExpression(expression);
31+
}
32+
33+
public Expression getExpression() {
34+
return expression;
35+
}
36+
37+
public void setExpression(Expression expression) {
38+
this.expression = Objects.requireNonNull(expression, "expression");
39+
}
40+
41+
public boolean isGeneratedAlways() {
42+
return generatedAlways;
43+
}
44+
45+
public void setGeneratedAlways(boolean generatedAlways) {
46+
this.generatedAlways = generatedAlways;
47+
}
48+
49+
/** Returns null when the SQL does not specify STORED or VIRTUAL. */
50+
public Storage getStorage() {
51+
return storage;
52+
}
53+
54+
public void setStorage(Storage storage) {
55+
this.storage = storage;
56+
}
57+
58+
public List<String> getTokens() {
59+
List<String> tokens = new ArrayList<>();
60+
if (generatedAlways) {
61+
tokens.add("GENERATED");
62+
tokens.add("ALWAYS");
63+
}
64+
tokens.add("AS");
65+
tokens.add("(" + expression + ")");
66+
if (storage != null) {
67+
tokens.add(storage.name());
68+
}
69+
return tokens;
70+
}
71+
72+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
73+
if (generatedAlways) {
74+
builder.append("GENERATED ALWAYS ");
75+
}
76+
builder.append("AS (");
77+
expressionPrinter.accept(expression);
78+
builder.append(')');
79+
if (storage != null) {
80+
builder.append(' ').append(storage);
81+
}
82+
return builder;
83+
}
84+
85+
@Override
86+
public String toString() {
87+
StringBuilder builder = new StringBuilder();
88+
return appendTo(builder, builder::append).toString();
89+
}
90+
}

src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public static void visit(TableElement element, Consumer<Expression> expressions,
133133
ColumnDefinition column = (ColumnDefinition) element;
134134
if (column.getColumnOptions() != null) {
135135
for (ColumnOption option : column.getColumnOptions()) {
136-
accept(option.getDefaultExpression(), expressions);
136+
option.visitExpressions(expressions);
137137
if (option.getForeignKeyReference() != null) {
138138
accept(option.getForeignKeyReference().getTable(), tables);
139139
}

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13084,12 +13084,44 @@ ColumnOption ColumnDefinitionOption(): {
1308413084
IdentityDefinition identity;
1308513085
NamedConstraint constraint;
1308613086
Expression defaultExpression;
13087+
GeneratedColumnDefinition generated;
13088+
ObjectNames collationNames;
13089+
String collation;
1308713090
} {
1308813091
(
1308913092
LOOKAHEAD({ isKeywordAhead("GENERATED")
1309013093
&& (getToken(4).kind == K_IDENTITY || getToken(5).kind == K_IDENTITY) })
1309113094
identity=IdentityDefinition() { option = ColumnOption.identity(identity); }
1309213095
|
13096+
LOOKAHEAD({ (isKeywordAhead("GENERATED") && getToken(2).kind == K_ALWAYS
13097+
&& getToken(3).kind == K_AS && "(".equals(getToken(4).image))
13098+
|| (getToken(1).kind == K_AS && "(".equals(getToken(2).image)) })
13099+
generated=GeneratedColumnDefinition() { option = ColumnOption.generated(generated); }
13100+
|
13101+
LOOKAHEAD(<K_NOT> <K_NULL>) <K_NOT> <K_NULL> { option = ColumnOption.nullability(false); }
13102+
|
13103+
LOOKAHEAD(<K_NULL>) <K_NULL> { option = ColumnOption.nullability(true); }
13104+
|
13105+
LOOKAHEAD(<K_VISIBLE>) <K_VISIBLE> { option = ColumnOption.visibility(true); }
13106+
|
13107+
LOOKAHEAD(<K_INVISIBLE>) <K_INVISIBLE> { option = ColumnOption.visibility(false); }
13108+
|
13109+
LOOKAHEAD(<K_AUTO_INCREMENT>) <K_AUTO_INCREMENT> { option = ColumnOption.autoIncrement(); }
13110+
|
13111+
LOOKAHEAD(<K_COLLATE>) <K_COLLATE>
13112+
( tk=<S_CHAR_LITERAL> { collation = tk.image; }
13113+
| collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } )
13114+
{ option = ColumnOption.collate(collation); }
13115+
|
13116+
LOOKAHEAD(<K_COMMENT>) <K_COMMENT> tk=<S_CHAR_LITERAL>
13117+
{ option = ColumnOption.comment(new StringValue(tk.image)); }
13118+
|
13119+
LOOKAHEAD(<K_ON> <K_UPDATE>) <K_ON> <K_UPDATE> defaultExpression=Expression()
13120+
{ option = ColumnOption.onUpdate(defaultExpression); }
13121+
|
13122+
LOOKAHEAD(<K_PRIMARY> <K_KEY>) <K_PRIMARY> <K_KEY>
13123+
{ option = ColumnOption.constraint(new NamedConstraint().withType("PRIMARY KEY")); }
13124+
|
1309313125
LOOKAHEAD(<K_UNIQUE>) constraint=ColumnUniqueConstraint()
1309413126
{ option = ColumnOption.constraint(constraint); }
1309513127
|
@@ -13118,6 +13150,28 @@ ColumnOption ColumnDefinitionOption(): {
1311813150
{ return option; }
1311913151
}
1312013152

13153+
GeneratedColumnDefinition GeneratedColumnDefinition():
13154+
{
13155+
Expression expression;
13156+
boolean generatedAlways = false;
13157+
GeneratedColumnDefinition.Storage storage = null;
13158+
GeneratedColumnDefinition result;
13159+
}
13160+
{
13161+
[ LOOKAHEAD({ isKeywordAhead("GENERATED") }) ContextualKeyword("GENERATED") <K_ALWAYS>
13162+
{ generatedAlways = true; } ]
13163+
<K_AS> "(" expression=Expression() ")"
13164+
[ LOOKAHEAD({ getToken(1).kind == K_STORED || isKeywordAhead("VIRTUAL") })
13165+
( <K_STORED> { storage = GeneratedColumnDefinition.Storage.STORED; }
13166+
| ContextualKeyword("VIRTUAL") { storage = GeneratedColumnDefinition.Storage.VIRTUAL; } ) ]
13167+
{
13168+
result = new GeneratedColumnDefinition(expression);
13169+
result.setGeneratedAlways(generatedAlways);
13170+
result.setStorage(storage);
13171+
return result;
13172+
}
13173+
}
13174+
1312113175
NamedConstraint ColumnUniqueConstraint():
1312213176
{
1312313177
NamedConstraint constraint = new NamedConstraint().withType("UNIQUE");

0 commit comments

Comments
 (0)