Skip to content

Commit b83e175

Browse files
committed
feat: structure column attributes and generated expressions
1 parent 6312f9e commit b83e175

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
@@ -12651,12 +12651,44 @@ ColumnOption ColumnDefinitionOption(): {
1265112651
IdentityDefinition identity;
1265212652
NamedConstraint constraint;
1265312653
Expression defaultExpression;
12654+
GeneratedColumnDefinition generated;
12655+
ObjectNames collationNames;
12656+
String collation;
1265412657
} {
1265512658
(
1265612659
LOOKAHEAD({ isKeywordAhead("GENERATED")
1265712660
&& (getToken(4).kind == K_IDENTITY || getToken(5).kind == K_IDENTITY) })
1265812661
identity=IdentityDefinition() { option = ColumnOption.identity(identity); }
1265912662
|
12663+
LOOKAHEAD({ (isKeywordAhead("GENERATED") && getToken(2).kind == K_ALWAYS
12664+
&& getToken(3).kind == K_AS && "(".equals(getToken(4).image))
12665+
|| (getToken(1).kind == K_AS && "(".equals(getToken(2).image)) })
12666+
generated=GeneratedColumnDefinition() { option = ColumnOption.generated(generated); }
12667+
|
12668+
LOOKAHEAD(<K_NOT> <K_NULL>) <K_NOT> <K_NULL> { option = ColumnOption.nullability(false); }
12669+
|
12670+
LOOKAHEAD(<K_NULL>) <K_NULL> { option = ColumnOption.nullability(true); }
12671+
|
12672+
LOOKAHEAD(<K_VISIBLE>) <K_VISIBLE> { option = ColumnOption.visibility(true); }
12673+
|
12674+
LOOKAHEAD(<K_INVISIBLE>) <K_INVISIBLE> { option = ColumnOption.visibility(false); }
12675+
|
12676+
LOOKAHEAD(<K_AUTO_INCREMENT>) <K_AUTO_INCREMENT> { option = ColumnOption.autoIncrement(); }
12677+
|
12678+
LOOKAHEAD(<K_COLLATE>) <K_COLLATE>
12679+
( tk=<S_CHAR_LITERAL> { collation = tk.image; }
12680+
| collationNames=RelObjectNames() { collation = String.join(".", collationNames.getNames()); } )
12681+
{ option = ColumnOption.collate(collation); }
12682+
|
12683+
LOOKAHEAD(<K_COMMENT>) <K_COMMENT> tk=<S_CHAR_LITERAL>
12684+
{ option = ColumnOption.comment(new StringValue(tk.image)); }
12685+
|
12686+
LOOKAHEAD(<K_ON> <K_UPDATE>) <K_ON> <K_UPDATE> defaultExpression=Expression()
12687+
{ option = ColumnOption.onUpdate(defaultExpression); }
12688+
|
12689+
LOOKAHEAD(<K_PRIMARY> <K_KEY>) <K_PRIMARY> <K_KEY>
12690+
{ option = ColumnOption.constraint(new NamedConstraint().withType("PRIMARY KEY")); }
12691+
|
1266012692
LOOKAHEAD(<K_UNIQUE>) constraint=ColumnUniqueConstraint()
1266112693
{ option = ColumnOption.constraint(constraint); }
1266212694
|
@@ -12685,6 +12717,28 @@ ColumnOption ColumnDefinitionOption(): {
1268512717
{ return option; }
1268612718
}
1268712719

12720+
GeneratedColumnDefinition GeneratedColumnDefinition():
12721+
{
12722+
Expression expression;
12723+
boolean generatedAlways = false;
12724+
GeneratedColumnDefinition.Storage storage = null;
12725+
GeneratedColumnDefinition result;
12726+
}
12727+
{
12728+
[ LOOKAHEAD({ isKeywordAhead("GENERATED") }) ContextualKeyword("GENERATED") <K_ALWAYS>
12729+
{ generatedAlways = true; } ]
12730+
<K_AS> "(" expression=Expression() ")"
12731+
[ LOOKAHEAD({ getToken(1).kind == K_STORED || isKeywordAhead("VIRTUAL") })
12732+
( <K_STORED> { storage = GeneratedColumnDefinition.Storage.STORED; }
12733+
| ContextualKeyword("VIRTUAL") { storage = GeneratedColumnDefinition.Storage.VIRTUAL; } ) ]
12734+
{
12735+
result = new GeneratedColumnDefinition(expression);
12736+
result.setGeneratedAlways(generatedAlways);
12737+
result.setStorage(storage);
12738+
return result;
12739+
}
12740+
}
12741+
1268812742
NamedConstraint ColumnUniqueConstraint():
1268912743
{
1269012744
NamedConstraint constraint = new NamedConstraint().withType("UNIQUE");

0 commit comments

Comments
 (0)