Skip to content

Commit d463fc0

Browse files
committed
feat: model MySQL expression-first CONVERT syntax
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 7f91819 commit d463fc0

4 files changed

Lines changed: 147 additions & 53 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414
import net.sf.jsqlparser.statement.create.table.ColDataType;
1515

1616
import java.util.Objects;
17+
import java.util.function.Consumer;
1718

1819
public class TranscodingFunction extends ASTNodeAccessImpl implements Expression {
1920
private String keyword = "CONVERT";
20-
private boolean isTranscodeStyle = true;
21+
22+
public enum Syntax {
23+
USING, TYPE_FIRST, TYPE_LAST
24+
}
25+
26+
private Syntax syntax = Syntax.USING;
2127
private ColDataType colDataType;
2228
private Expression expression;
2329
private String transcodingName;
@@ -39,15 +45,15 @@ public TranscodingFunction(String keyword, ColDataType colDataType, Expression e
3945
this.colDataType = colDataType;
4046
this.expression = expression;
4147
this.transcodingName = transcodingName;
42-
this.isTranscodeStyle = false;
48+
this.syntax = Syntax.TYPE_FIRST;
4349
}
4450

4551
public TranscodingFunction(ColDataType colDataType, Expression expression,
4652
String transcodingName) {
4753
this.colDataType = colDataType;
4854
this.expression = expression;
4955
this.transcodingName = transcodingName;
50-
this.isTranscodeStyle = false;
56+
this.syntax = Syntax.TYPE_FIRST;
5157
}
5258

5359
public TranscodingFunction() {
@@ -100,39 +106,45 @@ public TranscodingFunction setColDataType(ColDataType colDataType) {
100106
}
101107

102108
public boolean isTranscodeStyle() {
103-
return isTranscodeStyle;
109+
return syntax == Syntax.USING;
104110
}
105111

106112
public TranscodingFunction setTranscodeStyle(boolean transcodeStyle) {
107-
isTranscodeStyle = transcodeStyle;
113+
syntax = transcodeStyle ? Syntax.USING : Syntax.TYPE_FIRST;
108114
return this;
109115
}
110116

111117
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
112118
return expressionVisitor.visit(this, context);
113119
}
114120

121+
public Syntax getSyntax() {
122+
return syntax;
123+
}
124+
125+
public TranscodingFunction setSyntax(Syntax syntax) {
126+
this.syntax = Objects.requireNonNull(syntax);
127+
return this;
128+
}
129+
115130
public StringBuilder appendTo(StringBuilder builder) {
116-
if (isTranscodeStyle) {
117-
return builder
118-
.append(keyword)
119-
.append("( ")
120-
.append(expression)
121-
.append(" USING ")
122-
.append(transcodingName)
123-
.append(" )");
124-
} else {
125-
return builder
126-
.append(keyword)
127-
.append("( ")
128-
.append(colDataType)
129-
.append(", ")
130-
.append(expression)
131-
.append(transcodingName != null && !transcodingName.isEmpty()
132-
? ", " + transcodingName
133-
: "")
134-
.append(" )");
131+
return appendTo(builder, builder::append);
132+
}
133+
134+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
135+
builder.append(keyword).append("( ");
136+
if (syntax == Syntax.TYPE_FIRST) {
137+
builder.append(colDataType).append(", ");
138+
}
139+
expressionRenderer.accept(expression);
140+
if (syntax == Syntax.USING) {
141+
builder.append(" USING ").append(transcodingName);
142+
} else if (syntax == Syntax.TYPE_LAST) {
143+
builder.append(", ").append(colDataType);
144+
} else if (transcodingName != null && !transcodingName.isEmpty()) {
145+
builder.append(", ").append(transcodingName);
135146
}
147+
return builder.append(" )");
136148
}
137149

138150
@Override

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

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -743,29 +743,8 @@ public <S> StringBuilder visit(Select select, S context) {
743743

744744
@Override
745745
public <S> StringBuilder visit(TranscodingFunction transcodingFunction, S context) {
746-
if (transcodingFunction.isTranscodeStyle()) {
747-
builder.append(transcodingFunction.getKeyword());
748-
builder.append("( ");
749-
transcodingFunction.getExpression().accept(this, context);
750-
builder.append(" USING ")
751-
.append(transcodingFunction.getTranscodingName())
752-
.append(" )");
753-
} else {
754-
builder
755-
.append(transcodingFunction.getKeyword())
756-
.append("( ")
757-
.append(transcodingFunction.getColDataType())
758-
.append(", ");
759-
transcodingFunction.getExpression().accept(this, context);
760-
761-
String transCodingName = transcodingFunction.getTranscodingName();
762-
if (transCodingName != null && !transCodingName.isEmpty()) {
763-
builder.append(", ").append(transCodingName);
764-
}
765-
builder.append(" )");
766-
}
767-
768-
return builder;
746+
return transcodingFunction.appendTo(builder,
747+
expression -> expression.accept(this, context));
769748
}
770749

771750
public <S> StringBuilder visit(TrimFunction trimFunction, S context) {

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19545,7 +19545,8 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction :
1954519545
( keywordToken=<K_TRY_CONVERT> | keywordToken=<K_SAFE_CONVERT> | keywordToken=<K_CONVERT> )
1954619546
"("
1954719547
(
19548-
LOOKAHEAD(4) colDataType = ColDataType()
19548+
LOOKAHEAD(4, { !Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) })
19549+
colDataType = ColDataType()
1954919550
"," expression = Expression()
1955019551
[ "," style = <S_LONG> { transcodingName = style.image; } ]
1955119552

@@ -19555,11 +19556,14 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction :
1955519556
|
1955619557
(
1955719558
expression = Expression()
19558-
<K_USING> transcodingName=IdentifierChain()
19559-
19560-
{
19561-
transcodingFunction = new TranscodingFunction(expression, transcodingName);
19562-
}
19559+
(
19560+
<K_USING> transcodingName=IdentifierChain()
19561+
{ transcodingFunction = new TranscodingFunction(keywordToken.image, expression, transcodingName); }
19562+
|
19563+
"," colDataType=ColDataType()
19564+
{ transcodingFunction = new TranscodingFunction(keywordToken.image, colDataType, expression, null)
19565+
.setSyntax(TranscodingFunction.Syntax.TYPE_LAST); }
19566+
)
1956319567
)
1956419568
)
1956519569
")"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.expression;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import net.sf.jsqlparser.JSQLParserException;
14+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.schema.Column;
17+
import net.sf.jsqlparser.statement.select.PlainSelect;
18+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
19+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
20+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.ValueSource;
24+
25+
class MySqlConvertTest {
26+
@ParameterizedTest
27+
@ValueSource(strings = {"'42', SIGNED", "'-1', UNSIGNED INTEGER",
28+
"'3.14', DECIMAL(10,2)", "'xx', CHAR(16) CHARACTER SET 'utf8mb4'",
29+
"'xx', CHAR(16) CHARSET binary", "'2026-09-21', DATE",
30+
"NULL, CHAR", "amount, DECIMAL(12,4)", "COALESCE(amount, 0), SIGNED",
31+
"CONVERT('42', SIGNED), CHAR(10)", "?, SIGNED"})
32+
void representsExpressionBeforeType(String arguments) throws Exception {
33+
PlainSelect select = parse("SELECT CONVERT(" + arguments + ") FROM t", Dialect.MYSQL);
34+
TranscodingFunction convert = conversion(select);
35+
assertEquals(TranscodingFunction.Syntax.TYPE_LAST, convert.getSyntax());
36+
assertFalse(convert.isTranscodeStyle());
37+
assertNotNull(convert.getColDataType());
38+
roundTrip(select, Dialect.MYSQL);
39+
}
40+
41+
@Test
42+
void distinguishesDialectDependentOrderAndLegacyAccessors() throws Exception {
43+
TranscodingFunction mysql = conversion(parse("SELECT CONVERT(value, CHAR)", Dialect.MYSQL));
44+
assertInstanceOf(Column.class, mysql.getExpression());
45+
assertEquals("CHAR", mysql.getColDataType().getDataType());
46+
TranscodingFunction sqlserver = conversion(
47+
parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER));
48+
assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, sqlserver.getSyntax());
49+
assertEquals("120", sqlserver.getTranscodingName());
50+
roundTrip(parse("SELECT TRY_CONVERT(INT, value)", Dialect.SQLSERVER), Dialect.SQLSERVER);
51+
roundTrip(parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER),
52+
Dialect.SQLSERVER);
53+
TranscodingFunction using =
54+
conversion(parse("SELECT CONVERT(value USING utf8mb4)", Dialect.MYSQL));
55+
assertTrue(using.isTranscodeStyle());
56+
using.setTranscodeStyle(false);
57+
assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, using.getSyntax());
58+
using.setTranscodeStyle(true);
59+
assertEquals(TranscodingFunction.Syntax.USING, using.getSyntax());
60+
}
61+
62+
@Test
63+
void visitsOperandAndRendersTypeMutations() throws Exception {
64+
PlainSelect select = parse("SELECT CONVERT(42, SIGNED)", Dialect.MYSQL);
65+
conversion(select).getColDataType().setDataType("UNSIGNED");
66+
StringBuilder out = new StringBuilder();
67+
ExpressionDeParser visitor = new ExpressionDeParser() {
68+
@Override
69+
public <S> StringBuilder visit(LongValue value, S context) {
70+
return getBuilder().append(value.getValue() + 1);
71+
}
72+
};
73+
select.accept(new StatementDeParser(visitor, new SelectDeParser(), out), null);
74+
assertEquals("SELECT CONVERT( 43, UNSIGNED )", out.toString());
75+
roundTrip(select, Dialect.MYSQL);
76+
}
77+
78+
@ParameterizedTest
79+
@ValueSource(strings = {"SELECT CONVERT(1,)", "SELECT CONVERT(, SIGNED)",
80+
"SELECT CONVERT(1, SIGNED, 120)", "SELECT CONVERT(1 USING)"})
81+
void rejectsIncompleteArguments(String sql) {
82+
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL));
83+
}
84+
85+
private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException {
86+
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect));
87+
}
88+
89+
private static TranscodingFunction conversion(PlainSelect select) {
90+
return assertInstanceOf(TranscodingFunction.class, select.getSelectItem(0).getExpression());
91+
}
92+
93+
private static void roundTrip(PlainSelect select, Dialect dialect) throws Exception {
94+
StringBuilder out = new StringBuilder();
95+
select.accept(new StatementDeParser(out), null);
96+
assertEquals(select.toString(), out.toString());
97+
assertEquals(select.toString(), parse(out.toString(), dialect).toString());
98+
}
99+
}

0 commit comments

Comments
 (0)