Skip to content

Commit c85d896

Browse files
committed
feat: preserve column default expressions in table definitions
1 parent 0ecf096 commit c85d896

9 files changed

Lines changed: 342 additions & 61 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,9 +1115,19 @@ protected void toStringPartition(StringBuilder b) {
11151115
* Handles the general case for ADD, MODIFY, CHANGE, DROP (column), COMMENT, row-level security,
11161116
* and all field-based dispatch (columns, constraints, FK, UK, PK, index).
11171117
*/
1118+
protected void toStringGeneral(StringBuilder b) {
1119+
toStringGeneral(b, b::append);
1120+
}
1121+
1122+
/** Appends a column-definition action, including its common tail. */
1123+
public void appendColumnDefinitionsTo(StringBuilder b, Consumer<Expression> expressionPrinter) {
1124+
toStringGeneral(b, column -> column.appendTo(b, expressionPrinter));
1125+
appendCommonTail(b);
1126+
}
1127+
11181128
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity",
11191129
"PMD.ExcessiveMethodLength"})
1120-
protected void toStringGeneral(StringBuilder b) {
1130+
private void toStringGeneral(StringBuilder b, Consumer<ColumnDataType> columnPrinter) {
11211131
if (operation == AlterOperation.COMMENT_WITH_EQUAL_SIGN) {
11221132
b.append("COMMENT =").append(" ");
11231133
} else if (operation == AlterOperation.ENABLE_ROW_LEVEL_SECURITY) {
@@ -1170,7 +1180,12 @@ protected void toStringGeneral(StringBuilder b) {
11701180
if (useBrackets && colDataTypeList.size() == 1) {
11711181
b.append(" ( ");
11721182
}
1173-
b.append(PlainSelect.getStringList(colDataTypeList));
1183+
for (int i = 0; i < colDataTypeList.size(); i++) {
1184+
if (i > 0) {
1185+
b.append(", ");
1186+
}
1187+
columnPrinter.accept(colDataTypeList.get(i));
1188+
}
11741189
if (useBrackets && colDataTypeList.size() == 1) {
11751190
b.append(" ) ");
11761191
}
@@ -1490,13 +1505,25 @@ public ColumnDataType(
14901505

14911506
@Override
14921507
public String toString() {
1508+
StringBuilder builder = new StringBuilder();
1509+
appendTo(builder, builder::append);
1510+
return builder.toString();
1511+
}
1512+
1513+
@Override
1514+
public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
1515+
builder.append(getColumnName());
14931516
if (identityAlterations != null) {
1494-
return getColumnName() + " "
1495-
+ PlainSelect.getStringList(identityAlterations, false, false);
1517+
builder.append(' ')
1518+
.append(PlainSelect.getStringList(identityAlterations, false, false));
1519+
return;
1520+
}
1521+
builder.append(withType ? " TYPE " : getColDataType() == null ? "" : " ");
1522+
appendDataTypeAndSpecTo(builder, expressionPrinter);
1523+
if (usingExpression != null) {
1524+
builder.append(" USING ");
1525+
expressionPrinter.accept(usingExpression);
14961526
}
1497-
return getColumnName() + (withType ? " TYPE " : getColDataType() == null ? "" : " ")
1498-
+ toStringDataTypeAndSpec()
1499-
+ (usingExpression == null ? "" : " USING " + usingExpression);
15001527
}
15011528

15021529
@Override

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

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import net.sf.jsqlparser.statement.imprt.ImportColumn;
1313
import net.sf.jsqlparser.statement.select.PlainSelect;
14+
import net.sf.jsqlparser.expression.Expression;
1415

1516
import java.io.Serializable;
1617
import java.util.ArrayList;
@@ -19,6 +20,7 @@
1920
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.Optional;
23+
import java.util.function.Consumer;
2224

2325
/**
2426
* Globally used definition class for columns.
@@ -54,7 +56,7 @@ public ColumnDefinition(String columnName, ColDataType colDataType, List<String>
5456
/**
5557
* Returns raw specifications, or a token snapshot when structured options are present. Use the
5658
* option API or {@link #addColumnSpecs(Collection)} to append without discarding structured
57-
* references and constraints.
59+
* expressions, references and constraints. DEFAULT values use the expression's SQL rendering.
5860
*/
5961
public List<String> getColumnSpecs() {
6062
if (columnOptions != null) {
@@ -73,7 +75,7 @@ public void setColumnSpecs(List<String> list) {
7375
}
7476

7577
/**
76-
* Returns column options in source order, including structured references and MySQL
78+
* Returns column options in source order, including structured defaults, references and MySQL
7779
* {@code SERIAL DEFAULT VALUE}.
7880
*/
7981
public List<ColumnOption> getColumnOptions() {
@@ -134,17 +136,42 @@ public void setColumnName(String string) {
134136

135137
@Override
136138
public String toString() {
137-
return (columnName + " " + toStringDataTypeAndSpec()).trim();
139+
StringBuilder builder = new StringBuilder();
140+
appendTo(builder, builder::append);
141+
return builder.toString().trim();
142+
}
143+
144+
/** Appends a column definition using the supplied printer for structured expressions. */
145+
public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
146+
builder.append(columnName);
147+
if (colDataType != null || withOptions) {
148+
builder.append(' ');
149+
}
150+
appendDataTypeAndSpecTo(builder, expressionPrinter);
138151
}
139152

140153
public String toStringDataTypeAndSpec() {
141-
return (colDataType == null ? "" : colDataType)
142-
+ (withOptions ? "WITH OPTIONS" : "")
143-
+ (columnOptions != null && !columnOptions.isEmpty()
144-
? " " + PlainSelect.getStringList(columnOptions, false, false)
145-
: columnSpecs != null && !columnSpecs.isEmpty()
146-
? " " + PlainSelect.getStringList(columnSpecs, false, false)
147-
: "");
154+
StringBuilder builder = new StringBuilder();
155+
appendDataTypeAndSpecTo(builder, builder::append);
156+
return builder.toString();
157+
}
158+
159+
protected void appendDataTypeAndSpecTo(StringBuilder builder,
160+
Consumer<Expression> expressionPrinter) {
161+
if (colDataType != null) {
162+
builder.append(colDataType);
163+
}
164+
if (withOptions) {
165+
builder.append("WITH OPTIONS");
166+
}
167+
if (columnOptions != null) {
168+
for (ColumnOption option : columnOptions) {
169+
builder.append(' ');
170+
option.appendTo(builder, expressionPrinter);
171+
}
172+
} else if (columnSpecs != null && !columnSpecs.isEmpty()) {
173+
builder.append(' ').append(PlainSelect.getStringList(columnSpecs, false, false));
174+
}
148175
}
149176

150177
public ColumnDefinition withColumnName(String columnName) {

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,41 @@
1313
import java.util.Arrays;
1414
import java.util.Collections;
1515
import java.util.List;
16+
import java.util.Objects;
17+
import java.util.function.Consumer;
18+
import net.sf.jsqlparser.expression.Expression;
1619
import net.sf.jsqlparser.statement.select.PlainSelect;
1720

1821
/** A structured option following a column data type. */
1922
public class ColumnOption implements Serializable {
2023

2124
public enum Kind {
22-
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, OTHER
25+
SERIAL_DEFAULT_VALUE, REFERENCE, IDENTITY, CONSTRAINT, DEFAULT, OTHER
2326
}
2427

2528
private Kind kind = Kind.OTHER;
2629
private List<String> tokens;
2730
private ForeignKeyReference foreignKeyReference;
2831
private IdentityDefinition identityDefinition;
2932
private Index constraint;
33+
private Expression defaultExpression;
34+
35+
/** Creates a DEFAULT option. Use a NullValue expression for SQL NULL. */
36+
public static ColumnOption defaultValue(Expression expression) {
37+
ColumnOption option = new ColumnOption();
38+
option.kind = Kind.DEFAULT;
39+
option.setDefaultExpression(expression);
40+
return option;
41+
}
42+
43+
public Expression getDefaultExpression() {
44+
return defaultExpression;
45+
}
46+
47+
/** Replaces the expression of a DEFAULT option created by {@link #defaultValue(Expression)}. */
48+
public void setDefaultExpression(Expression expression) {
49+
defaultExpression = Objects.requireNonNull(expression, "defaultExpression");
50+
}
3051

3152
public static ColumnOption identity(IdentityDefinition definition) {
3253
ColumnOption option = new ColumnOption();
@@ -78,6 +99,9 @@ public Kind getKind() {
7899
}
79100

80101
public List<String> getTokens() {
102+
if (kind == Kind.DEFAULT) {
103+
return Arrays.asList("DEFAULT", String.valueOf(defaultExpression));
104+
}
81105
return kind == Kind.OTHER || kind == Kind.SERIAL_DEFAULT_VALUE ? tokens
82106
: Collections.singletonList(toString());
83107
}
@@ -88,15 +112,30 @@ public ForeignKeyReference getForeignKeyReference() {
88112

89113
@Override
90114
public String toString() {
115+
StringBuilder builder = new StringBuilder();
116+
appendTo(builder, builder::append);
117+
return builder.toString();
118+
}
119+
120+
/** Appends the option using the supplied printer for structured expressions. */
121+
public void appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
91122
switch (kind) {
123+
case DEFAULT:
124+
builder.append("DEFAULT ");
125+
expressionPrinter.accept(defaultExpression);
126+
break;
92127
case REFERENCE:
93-
return foreignKeyReference.toString();
128+
builder.append(foreignKeyReference);
129+
break;
94130
case IDENTITY:
95-
return identityDefinition.toString();
131+
builder.append(identityDefinition);
132+
break;
96133
case CONSTRAINT:
97-
return constraint.toString();
134+
constraint.appendTo(builder, expressionPrinter);
135+
break;
98136
default:
99-
return PlainSelect.getStringList(tokens, false, false);
137+
builder.append(PlainSelect.getStringList(tokens, false, false));
138+
break;
100139
}
101140
}
102141
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +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);
136137
if (option.getForeignKeyReference() != null) {
137138
accept(option.getForeignKeyReference().getTable(), tables);
138139
}

src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,12 @@ private void deParseAction(AlterExpression action) {
8181
expression -> expression.accept(expressionVisitor, null));
8282
return;
8383
}
84-
if (action.getColDataTypeList() == null || action.getColDataTypeList().size() != 1
85-
|| action.getColDataTypeList().get(0).getUsingExpression() == null) {
84+
if (action.getColDataTypeList() != null) {
85+
action.appendColumnDefinitionsTo(builder,
86+
expression -> expression.accept(expressionVisitor, null));
87+
} else {
8688
builder.append(action);
87-
return;
88-
}
89-
AlterExpression.ColumnDataType column = action.getColDataTypeList().get(0);
90-
builder.append(action.getOperation()).append(' ');
91-
if (action.hasColumn()) {
92-
builder.append("COLUMN ");
93-
}
94-
if (action.isUsingIfExists()) {
95-
builder.append("IF EXISTS ");
9689
}
97-
builder.append(column.getColumnName()).append(column.isWithType() ? " TYPE " : " ")
98-
.append(column.toStringDataTypeAndSpec()).append(" USING ");
99-
column.getUsingExpression().accept(expressionVisitor, null);
100-
deParseTail(action);
10190
}
10291

10392
private void deParseTail(AlterExpression action) {

src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import net.sf.jsqlparser.expression.ExpressionVisitor;
1313
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
14-
import net.sf.jsqlparser.statement.create.table.ColumnOption;
1514
import net.sf.jsqlparser.statement.create.table.Index;
1615
import net.sf.jsqlparser.statement.create.table.TableElement;
1716

@@ -30,30 +29,12 @@ public void deParse(TableElement element) {
3029
if (element instanceof Index) {
3130
((Index) element).appendTo(builder,
3231
expression -> expression.accept(expressionVisitor, null));
33-
} else if (element instanceof ColumnDefinition
34-
&& ((ColumnDefinition) element).getColumnOptions() != null) {
35-
deParseColumn((ColumnDefinition) element);
32+
} else if (element instanceof ColumnDefinition) {
33+
((ColumnDefinition) element).appendTo(builder,
34+
expression -> expression.accept(expressionVisitor, null));
3635
} else {
3736
builder.append(element);
3837
}
3938
}
4039

41-
private void deParseColumn(ColumnDefinition column) {
42-
builder.append(column.getColumnName());
43-
if (column.getColDataType() != null) {
44-
builder.append(' ').append(column.getColDataType());
45-
}
46-
if (column.isWithOptions()) {
47-
builder.append(" WITH OPTIONS");
48-
}
49-
for (ColumnOption option : column.getColumnOptions()) {
50-
builder.append(' ');
51-
if (option.getConstraint() != null) {
52-
deParse(option.getConstraint());
53-
} else {
54-
builder.append(option);
55-
}
56-
}
57-
}
58-
5940
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1702,8 +1702,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
17021702
case K_MATCH_PHRASE: // MATCH_PHRASE
17031703
case K_MATCH_PHRASE_PREFIX: // MATCH_PHRASE_PREFIX
17041704
case K_MATCH_REGEXP: // MATCH_REGEXP
1705-
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
17061705
return true;
1706+
case K_NOT: // NOT IN / NOT BETWEEN / NOT LIKE / NOT ISNULL / NOT SIMILAR
1707+
// A column's NOT NULL constraint starts after its DEFAULT expression.
1708+
return getToken(2).kind != K_NULL;
17071709
// Oracle (+) before IN: col(+) IN (...)
17081710
case OPENING_BRACKET:
17091711
return getToken(2).image.equals("+");
@@ -12566,6 +12568,7 @@ ColumnOption ColumnDefinitionOption(): {
1256612568
ColumnOption option;
1256712569
IdentityDefinition identity;
1256812570
NamedConstraint constraint;
12571+
Expression defaultExpression;
1256912572
} {
1257012573
(
1257112574
LOOKAHEAD({ isKeywordAhead("GENERATED")
@@ -12583,6 +12586,17 @@ ColumnOption ColumnDefinitionOption(): {
1258312586
LOOKAHEAD(<K_REFERENCES>) reference=ForeignKeyReferenceSpec()
1258412587
{ option = ColumnOption.reference(reference); }
1258512588
|
12589+
LOOKAHEAD(<K_DROP> <K_DEFAULT>) <K_DROP> <K_DEFAULT>
12590+
{ option = ColumnOption.raw("DROP", "DEFAULT"); }
12591+
|
12592+
LOOKAHEAD(<K_DEFAULT> <K_ON> <K_NULL>) <K_DEFAULT> <K_ON> <K_NULL>
12593+
{ option = ColumnOption.raw("DEFAULT", "ON", "NULL"); }
12594+
|
12595+
LOOKAHEAD(<K_DEFAULT>) <K_DEFAULT> { option = ColumnOption.raw("DEFAULT"); }
12596+
// Db2 permits an omitted default value. Keep these implicit defaults raw.
12597+
[ LOOKAHEAD(2, { !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) })
12598+
defaultExpression=Expression() { option = ColumnOption.defaultValue(defaultExpression); } ]
12599+
|
1258612600
parameter=ColumnDefinitionParameter()
1258712601
{ option = ColumnOption.raw(parameter); }
1258812602
)

src/test/java/net/sf/jsqlparser/statement/alter/AlterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ public void testAlterTableDefaultValueTrueIssue926() throws JSQLParserException
971971

972972
// There shall be no COLUMN where there is no COLUMN
973973
assertStatementCanBeDeparsedAs(parsed,
974-
"ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT FALSE");
974+
"ALTER TABLE my_table ADD some_column BOOLEAN DEFAULT false");
975975
}
976976

977977
private void assertReferentialActionOnConstraint(Alter parsed, Action onUpdate,

0 commit comments

Comments
 (0)