Skip to content

Commit 60b80a2

Browse files
committed
fix: end column DEFAULT values before a top-level COLLATE
In MySQL and PostgreSQL column definitions, a COLLATE after the DEFAULT value is a column attribute: MySQL's DEFAULT takes a literal and PostgreSQL's a b_expr, and neither includes COLLATE. The column DEFAULT parsed a full expression, so DEFAULT 'N' COLLATE x became one DEFAULT option holding a CollateExpression. Parse column DEFAULT values with a ColumnDefaultValue production that stops before a COLLATE at its top level. That COLLATE becomes the column's COLLATE option. A COLLATE inside parentheses, function arguments or CASE stays in the expression. SET DEFAULT, an a_expr in PostgreSQL, and CREATE DOMAIN are unchanged. SQL output does not change. The nesting walk of slice subscripts moves to the package-private TokenNesting, like RoutineBodyBoundary, and serves both checks. The generated CCJSqlParser class is close to the size JaCoCo can still instrument. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 90ac3a7 commit 60b80a2

4 files changed

Lines changed: 187 additions & 19 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.parser;
11+
12+
import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*;
13+
14+
/**
15+
* Token nesting checks used by grammar lookaheads. Kept outside the generated parser, whose class
16+
* is close to the size JaCoCo can still instrument.
17+
*/
18+
final class TokenNesting {
19+
private TokenNesting() {}
20+
21+
/**
22+
* Whether the target is not nested in brackets, braces, parentheses or a CASE expression opened
23+
* from the first token on.
24+
*/
25+
static boolean isTopLevelFrom(Token first, Token target) {
26+
int depth = 0;
27+
for (Token t = first; t != null && t != target; t = t.next) {
28+
if (t.kind == K_CASE || "(".equals(t.image) || "[".equals(t.image)
29+
|| "{".equals(t.image)) {
30+
depth++;
31+
} else if (t.kind == K_END || ")".equals(t.image) || "]".equals(t.image)
32+
|| "}".equals(t.image)) {
33+
depth--;
34+
}
35+
}
36+
return depth == 0;
37+
}
38+
}

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,19 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
224224
*/
225225
private boolean isSliceColonAhead() {
226226
Token colon = getToken(1);
227-
if (subscriptOpenings.isEmpty() || !":".equals(colon.image)) {
228-
return false;
229-
}
230-
int depth = 0;
231-
for (Token t = subscriptOpenings.peek().next; t != null && t != colon; t = t.next) {
232-
if (t.kind == K_CASE || "(".equals(t.image) || "[".equals(t.image)
233-
|| "{".equals(t.image)) {
234-
depth++;
235-
} else if (t.kind == K_END || ")".equals(t.image) || "]".equals(t.image)
236-
|| "}".equals(t.image)) {
237-
depth--;
238-
}
239-
}
240-
return depth == 0;
227+
return !subscriptOpenings.isEmpty() && ":".equals(colon.image)
228+
&& TokenNesting.isTopLevelFrom(subscriptOpenings.peek().next, colon);
229+
}
230+
231+
// first tokens of the enclosing column DEFAULT values: a COLLATE at their top level is the
232+
// column's own attribute, as in MySQL's column_attribute and PostgreSQL's ColQualList
233+
private final Deque<Token> columnDefaultStarts = new ArrayDeque<Token>();
234+
235+
/** Whether the pending COLLATE follows an unparenthesized column DEFAULT value. */
236+
private boolean isColumnCollateAhead() {
237+
Token collate = getToken(1);
238+
return !columnDefaultStarts.isEmpty() && collate.kind == K_COLLATE
239+
&& TokenNesting.isTopLevelFrom(columnDefaultStarts.peek(), collate);
241240
}
242241

243242
private Expression createOutputArgument(Expression argument) throws ParseException {
@@ -13149,7 +13148,7 @@ Expression PrimaryExpression() #PrimaryExpression:
1314913148
)
1315013149

1315113150
[
13152-
LOOKAHEAD(2) <K_COLLATE> (token=<S_CHAR_LITERAL> | token=<S_QUOTED_IDENTIFIER> | token=<S_IDENTIFIER>) { retval = new CollateExpression(retval, token.image); }
13151+
LOOKAHEAD(2, { !isColumnCollateAhead() }) <K_COLLATE> (token=<S_CHAR_LITERAL> | token=<S_QUOTED_IDENTIFIER> | token=<S_IDENTIFIER>) { retval = new CollateExpression(retval, token.image); }
1315313152
]
1315413153

1315513154
[
@@ -16389,14 +16388,33 @@ ColumnOption ColumnDefinitionOption(): {
1638916388
LOOKAHEAD(<K_DEFAULT>) <K_DEFAULT> { option = ColumnOption.raw("DEFAULT"); }
1639016389
// Db2 permits an omitted default value. Keep these implicit defaults raw.
1639116390
[ LOOKAHEAD(2, { !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) })
16392-
defaultExpression=Expression() { option = ColumnOption.defaultValue(defaultExpression); } ]
16391+
defaultExpression=ColumnDefaultValue() { option = ColumnOption.defaultValue(defaultExpression); } ]
1639316392
|
1639416393
parameter=ColumnDefinitionParameter()
1639516394
{ option = ColumnOption.raw(parameter); }
1639616395
)
1639716396
{ return option; }
1639816397
}
1639916398

16399+
/**
16400+
* An explicit column DEFAULT value. MySQL's DEFAULT literal and PostgreSQL's DEFAULT b_expr exclude
16401+
* COLLATE, so the value ends before a COLLATE at its top level, which is the column's own
16402+
* attribute. A COLLATE inside parentheses, function arguments or CASE stays in the expression.
16403+
*/
16404+
Expression ColumnDefaultValue():
16405+
{
16406+
Expression expression;
16407+
}
16408+
{
16409+
{ columnDefaultStarts.push(getToken(1)); }
16410+
try {
16411+
expression=Expression()
16412+
} finally {
16413+
columnDefaultStarts.pop();
16414+
}
16415+
{ return expression; }
16416+
}
16417+
1640016418
GeneratedColumnDefinition GeneratedColumnDefinition():
1640116419
{
1640216420
Expression expression;

‎src/site/sphinx/usage.rst‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,18 @@ Charset remains in ``ColDataType.getCharacterSet()``; column collation is in its
935935
COLLATE option. Consumers can map both directly without reconstructing tokens.
936936
StringValue comment bodies retain their SQL escape representation.
937937

938+
A column DEFAULT value ends before a COLLATE at its top level, which becomes the
939+
column's COLLATE option: MySQL's DEFAULT literal and PostgreSQL's DEFAULT ``b_expr``
940+
do not include COLLATE. A COLLATE inside parentheses, function arguments or a CASE
941+
expression stays in the default expression. ``SET DEFAULT`` actions and CREATE
942+
DOMAIN keep an expression COLLATE.
943+
944+
Migration: ``DEFAULT 'N' COLLATE utf8mb4_bin`` in a column definition previously
945+
produced one DEFAULT option holding a ``CollateExpression``. It now produces a
946+
DEFAULT option with ``'N'`` and a COLLATE option with ``getCollation()``;
947+
``getColumnSpecs()`` returns ``[DEFAULT, 'N', COLLATE, utf8mb4_bin]``. SQL output
948+
is unchanged.
949+
938950
MySQL column attributes have their own kinds: ``SRID`` (``getSrid()``),
939951
``COLUMN_FORMAT`` (``getColumnFormat()``: FIXED, DYNAMIC or DEFAULT), and
940952
``ENGINE_ATTRIBUTE``/``SECONDARY_ENGINE_ATTRIBUTE`` (``getEngineAttribute()``

‎src/test/java/net/sf/jsqlparser/statement/create/ColumnDefaultExpressionTest.java‎

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,48 @@
99
*/
1010
package net.sf.jsqlparser.statement.create;
1111

12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
1213
import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed;
1314
import static org.assertj.core.api.Assertions.assertThat;
1415
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1517
import static org.junit.jupiter.api.Assertions.assertNotNull;
1618
import static org.junit.jupiter.api.Assertions.assertNull;
1719
import static org.junit.jupiter.api.Assertions.assertThrows;
18-
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.List;
23+
import java.util.function.Consumer;
24+
import java.util.stream.Stream;
2225
import net.sf.jsqlparser.JSQLParserException;
26+
import net.sf.jsqlparser.expression.CollateExpression;
2327
import net.sf.jsqlparser.expression.Expression;
2428
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
2529
import net.sf.jsqlparser.expression.LongValue;
30+
import net.sf.jsqlparser.expression.StringValue;
31+
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
32+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
33+
import net.sf.jsqlparser.parser.CCJSqlParser;
2634
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
2735
import net.sf.jsqlparser.parser.feature.Feature;
28-
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
29-
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
3036
import net.sf.jsqlparser.statement.Statement;
3137
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
3238
import net.sf.jsqlparser.statement.alter.Alter;
39+
import net.sf.jsqlparser.statement.alter.AlterDomain;
40+
import net.sf.jsqlparser.statement.alter.AlterRelation;
41+
import net.sf.jsqlparser.statement.create.domain.CreateDomain;
3342
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
3443
import net.sf.jsqlparser.statement.create.table.ColumnOption;
3544
import net.sf.jsqlparser.statement.create.table.CreateTable;
3645
import net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
3746
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
3847
import net.sf.jsqlparser.util.deparser.SelectDeParser;
3948
import net.sf.jsqlparser.util.deparser.StatementDeParser;
49+
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
4050
import org.junit.jupiter.api.Test;
4151
import org.junit.jupiter.params.ParameterizedTest;
52+
import org.junit.jupiter.params.provider.Arguments;
53+
import org.junit.jupiter.params.provider.MethodSource;
4254
import org.junit.jupiter.params.provider.ValueSource;
4355

4456
class ColumnDefaultExpressionTest {
@@ -161,6 +173,94 @@ void neighboringAndLegacyColumnOptionsStillRoundTrip(String sql) throws JSQLPars
161173
assertRoundTrip(CCJSqlParserUtil.parse(sql));
162174
}
163175

176+
@ParameterizedTest
177+
@ValueSource(
178+
strings = {"CREATE TABLE t (c VARCHAR(30) ", "ALTER TABLE t ADD COLUMN c VARCHAR(30) ",
179+
"ALTER TABLE t MODIFY COLUMN c VARCHAR(30) ",
180+
"ALTER TABLE t CHANGE COLUMN old_c c VARCHAR(30) "})
181+
void collateAfterDefaultIsAColumnAttribute(String prefix) throws JSQLParserException {
182+
String sql = prefix + "NOT NULL DEFAULT 'N' COLLATE utf8mb4_unicode_ci COMMENT 'flag'"
183+
+ (prefix.startsWith("CREATE") ? ")" : "");
184+
List<Consumer<CCJSqlParser>> configurations = Arrays.asList(parser -> {
185+
}, parser -> parser.withDialect(Dialect.MYSQL));
186+
for (Consumer<CCJSqlParser> configuration : configurations) {
187+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, configuration);
188+
ColumnDefinition column = firstColumn(statement);
189+
assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind)
190+
.containsExactly(ColumnOption.Kind.NULLABILITY, ColumnOption.Kind.DEFAULT,
191+
ColumnOption.Kind.COLLATE, ColumnOption.Kind.COMMENT);
192+
assertInstanceOf(StringValue.class, defaultOption(column).getDefaultExpression());
193+
assertEquals("utf8mb4_unicode_ci", column.getColumnOptions().get(2).getCollation());
194+
assertThat(column.getColumnSpecs()).containsExactly("NOT", "NULL", "DEFAULT", "'N'",
195+
"COLLATE", "utf8mb4_unicode_ci", "COMMENT", "'flag'");
196+
assertRoundTrip(statement);
197+
}
198+
}
199+
200+
static Stream<Arguments> defaultValuesBeforeCollate() {
201+
return Stream.of(Arguments.of(Dialect.MYSQL, "_utf8mb4'N'", "utf8mb4_bin"),
202+
Arguments.of(Dialect.MYSQL, "'N'", "'utf8mb4_bin'"),
203+
Arguments.of(Dialect.MYSQL, "1", "utf8mb4_bin"),
204+
Arguments.of(Dialect.MYSQL, "-1", "utf8mb4_bin"),
205+
Arguments.of(Dialect.POSTGRESQL, "'a' || 'b'", "\"C\""),
206+
Arguments.of(Dialect.POSTGRESQL, "lower('A' COLLATE \"C\")", "\"C\""),
207+
Arguments.of(Dialect.POSTGRESQL, "CASE WHEN true THEN 'a' END", "\"C\""),
208+
Arguments.of(Dialect.POSTGRESQL, "'x'::text", "\"C\""),
209+
Arguments.of(Dialect.POSTGRESQL, "'x'", "pg_catalog.\"default\""));
210+
}
211+
212+
@ParameterizedTest
213+
@MethodSource("defaultValuesBeforeCollate")
214+
void defaultValueEndsBeforeTopLevelCollate(Dialect dialect, String value, String collation)
215+
throws JSQLParserException {
216+
String definition = " VARCHAR(30) DEFAULT " + value + " COLLATE " + collation + " NOT NULL";
217+
String alter = dialect == Dialect.MYSQL ? "ALTER TABLE t MODIFY COLUMN c"
218+
: "ALTER TABLE t ADD COLUMN c";
219+
for (String sql : Arrays.asList("CREATE TABLE t (c" + definition + ")",
220+
alter + definition)) {
221+
ColumnDefinition column = firstColumn(assertSqlCanBeParsedAndDeparsed(sql, true,
222+
parser -> parser.withDialect(dialect)));
223+
assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind)
224+
.containsExactly(ColumnOption.Kind.DEFAULT, ColumnOption.Kind.COLLATE,
225+
ColumnOption.Kind.NULLABILITY);
226+
assertEquals(value, defaultOption(column).getDefaultExpression().toString());
227+
assertEquals(collation, column.getColumnOptions().get(1).getCollation());
228+
}
229+
}
230+
231+
@Test
232+
void parenthesizedCollateStaysInTheDefaultValue() throws JSQLParserException {
233+
ColumnDefinition column = firstColumn(assertSqlCanBeParsedAndDeparsed(
234+
"ALTER TABLE t MODIFY COLUMN c VARCHAR(3) DEFAULT ('N' COLLATE utf8mb4_bin) NOT NULL",
235+
true, parser -> parser.withDialect(Dialect.MYSQL)));
236+
assertThat(column.getColumnOptions()).extracting(ColumnOption::getKind)
237+
.containsExactly(ColumnOption.Kind.DEFAULT, ColumnOption.Kind.NULLABILITY);
238+
Expression value = defaultOption(column).getDefaultExpression();
239+
assertInstanceOf(ParenthesedExpressionList.class, value);
240+
assertInstanceOf(CollateExpression.class, ((ParenthesedExpressionList<?>) value).get(0));
241+
}
242+
243+
@Test
244+
void setDefaultAndDomainDefaultsKeepTheExpressionCollate() throws JSQLParserException {
245+
// PostgreSQL's SET DEFAULT takes an a_expr, which includes COLLATE. CREATE DOMAIN is
246+
// unchanged.
247+
Consumer<CCJSqlParser> postgres = parser -> parser.withDialect(Dialect.POSTGRESQL);
248+
Alter table = (Alter) assertSqlCanBeParsedAndDeparsed(
249+
"ALTER TABLE t ALTER COLUMN c SET DEFAULT 'x' COLLATE \"C\"", true, postgres);
250+
assertInstanceOf(CollateExpression.class, table.getAlterExpressions().get(0)
251+
.getColumnDefaults().get(0).getDefaultExpression());
252+
AlterRelation view = (AlterRelation) assertSqlCanBeParsedAndDeparsed(
253+
"ALTER VIEW v ALTER COLUMN c SET DEFAULT 'x' COLLATE \"C\"", true, postgres);
254+
assertInstanceOf(CollateExpression.class,
255+
view.getActions().get(0).getDefaultExpression());
256+
AlterDomain alterDomain = (AlterDomain) assertSqlCanBeParsedAndDeparsed(
257+
"ALTER DOMAIN d SET DEFAULT 'x' COLLATE \"C\"", true, postgres);
258+
assertInstanceOf(CollateExpression.class, alterDomain.getDefaultExpression());
259+
CreateDomain createDomain = (CreateDomain) assertSqlCanBeParsedAndDeparsed(
260+
"CREATE DOMAIN d AS text DEFAULT 'x' COLLATE \"C\"", true, postgres);
261+
assertInstanceOf(CollateExpression.class, createDomain.getDefaultExpression());
262+
}
263+
164264
@ParameterizedTest
165265
@ValueSource(strings = {"CREATE TABLE t (a INT DEFAULT (1 + 2",
166266
"CREATE TABLE t (a INT DEFAULT (1 +))",

0 commit comments

Comments
 (0)