Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 36 additions & 24 deletions src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
import net.sf.jsqlparser.statement.create.table.ColDataType;

import java.util.Objects;
import java.util.function.Consumer;

public class TranscodingFunction extends ASTNodeAccessImpl implements Expression {
private String keyword = "CONVERT";
private boolean isTranscodeStyle = true;

public enum Syntax {
USING, TYPE_FIRST, TYPE_LAST
}

private Syntax syntax = Syntax.USING;
private ColDataType colDataType;
private Expression expression;
private String transcodingName;
Expand All @@ -39,15 +45,15 @@ public TranscodingFunction(String keyword, ColDataType colDataType, Expression e
this.colDataType = colDataType;
this.expression = expression;
this.transcodingName = transcodingName;
this.isTranscodeStyle = false;
this.syntax = Syntax.TYPE_FIRST;
}

public TranscodingFunction(ColDataType colDataType, Expression expression,
String transcodingName) {
this.colDataType = colDataType;
this.expression = expression;
this.transcodingName = transcodingName;
this.isTranscodeStyle = false;
this.syntax = Syntax.TYPE_FIRST;
}

public TranscodingFunction() {
Expand Down Expand Up @@ -100,39 +106,45 @@ public TranscodingFunction setColDataType(ColDataType colDataType) {
}

public boolean isTranscodeStyle() {
return isTranscodeStyle;
return syntax == Syntax.USING;
}

public TranscodingFunction setTranscodeStyle(boolean transcodeStyle) {
isTranscodeStyle = transcodeStyle;
syntax = transcodeStyle ? Syntax.USING : Syntax.TYPE_FIRST;
return this;
}

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

public Syntax getSyntax() {
return syntax;
}

public TranscodingFunction setSyntax(Syntax syntax) {
this.syntax = Objects.requireNonNull(syntax);
return this;
}

public StringBuilder appendTo(StringBuilder builder) {
if (isTranscodeStyle) {
return builder
.append(keyword)
.append("( ")
.append(expression)
.append(" USING ")
.append(transcodingName)
.append(" )");
} else {
return builder
.append(keyword)
.append("( ")
.append(colDataType)
.append(", ")
.append(expression)
.append(transcodingName != null && !transcodingName.isEmpty()
? ", " + transcodingName
: "")
.append(" )");
return appendTo(builder, builder::append);
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
builder.append(keyword).append("( ");
if (syntax == Syntax.TYPE_FIRST) {
builder.append(colDataType).append(", ");
}
expressionRenderer.accept(expression);
if (syntax == Syntax.USING) {
builder.append(" USING ").append(transcodingName);
} else if (syntax == Syntax.TYPE_LAST) {
builder.append(", ").append(colDataType);
} else if (transcodingName != null && !transcodingName.isEmpty()) {
builder.append(", ").append(transcodingName);
}
return builder.append(" )");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,29 +743,8 @@ public <S> StringBuilder visit(Select select, S context) {

@Override
public <S> StringBuilder visit(TranscodingFunction transcodingFunction, S context) {
if (transcodingFunction.isTranscodeStyle()) {
builder.append(transcodingFunction.getKeyword());
builder.append("( ");
transcodingFunction.getExpression().accept(this, context);
builder.append(" USING ")
.append(transcodingFunction.getTranscodingName())
.append(" )");
} else {
builder
.append(transcodingFunction.getKeyword())
.append("( ")
.append(transcodingFunction.getColDataType())
.append(", ");
transcodingFunction.getExpression().accept(this, context);

String transCodingName = transcodingFunction.getTranscodingName();
if (transCodingName != null && !transCodingName.isEmpty()) {
builder.append(", ").append(transCodingName);
}
builder.append(" )");
}

return builder;
return transcodingFunction.appendTo(builder,
expression -> expression.accept(this, context));
}

public <S> StringBuilder visit(TrimFunction trimFunction, S context) {
Expand Down
16 changes: 10 additions & 6 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -19545,7 +19545,8 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction :
( keywordToken=<K_TRY_CONVERT> | keywordToken=<K_SAFE_CONVERT> | keywordToken=<K_CONVERT> )
"("
(
LOOKAHEAD(4) colDataType = ColDataType()
LOOKAHEAD(4, { !Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) })
colDataType = ColDataType()
"," expression = Expression()
[ "," style = <S_LONG> { transcodingName = style.image; } ]

Expand All @@ -19555,11 +19556,14 @@ TranscodingFunction TranscodingFunction() #TranscodingFunction :
|
(
expression = Expression()
<K_USING> transcodingName=IdentifierChain()

{
transcodingFunction = new TranscodingFunction(expression, transcodingName);
}
(
<K_USING> transcodingName=IdentifierChain()
{ transcodingFunction = new TranscodingFunction(keywordToken.image, expression, transcodingName); }
|
"," colDataType=ColDataType()
{ transcodingFunction = new TranscodingFunction(keywordToken.image, colDataType, expression, null)
.setSyntax(TranscodingFunction.Syntax.TYPE_LAST); }
)
)
)
")"
Expand Down
99 changes: 99 additions & 0 deletions src/test/java/net/sf/jsqlparser/expression/MySqlConvertTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.expression;

import static org.junit.jupiter.api.Assertions.*;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class MySqlConvertTest {
@ParameterizedTest
@ValueSource(strings = {"'42', SIGNED", "'-1', UNSIGNED INTEGER",
"'3.14', DECIMAL(10,2)", "'xx', CHAR(16) CHARACTER SET 'utf8mb4'",
"'xx', CHAR(16) CHARSET binary", "'2026-09-21', DATE",
"NULL, CHAR", "amount, DECIMAL(12,4)", "COALESCE(amount, 0), SIGNED",
"CONVERT('42', SIGNED), CHAR(10)", "?, SIGNED"})
void representsExpressionBeforeType(String arguments) throws Exception {
PlainSelect select = parse("SELECT CONVERT(" + arguments + ") FROM t", Dialect.MYSQL);
TranscodingFunction convert = conversion(select);
assertEquals(TranscodingFunction.Syntax.TYPE_LAST, convert.getSyntax());
assertFalse(convert.isTranscodeStyle());
assertNotNull(convert.getColDataType());
roundTrip(select, Dialect.MYSQL);
}

@Test
void distinguishesDialectDependentOrderAndLegacyAccessors() throws Exception {
TranscodingFunction mysql = conversion(parse("SELECT CONVERT(value, CHAR)", Dialect.MYSQL));
assertInstanceOf(Column.class, mysql.getExpression());
assertEquals("CHAR", mysql.getColDataType().getDataType());
TranscodingFunction sqlserver = conversion(
parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER));
assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, sqlserver.getSyntax());
assertEquals("120", sqlserver.getTranscodingName());
roundTrip(parse("SELECT TRY_CONVERT(INT, value)", Dialect.SQLSERVER), Dialect.SQLSERVER);
roundTrip(parse("SELECT CONVERT(VARCHAR(10), value, 120)", Dialect.SQLSERVER),
Dialect.SQLSERVER);
TranscodingFunction using =
conversion(parse("SELECT CONVERT(value USING utf8mb4)", Dialect.MYSQL));
assertTrue(using.isTranscodeStyle());
using.setTranscodeStyle(false);
assertEquals(TranscodingFunction.Syntax.TYPE_FIRST, using.getSyntax());
using.setTranscodeStyle(true);
assertEquals(TranscodingFunction.Syntax.USING, using.getSyntax());
}

@Test
void visitsOperandAndRendersTypeMutations() throws Exception {
PlainSelect select = parse("SELECT CONVERT(42, SIGNED)", Dialect.MYSQL);
conversion(select).getColDataType().setDataType("UNSIGNED");
StringBuilder out = new StringBuilder();
ExpressionDeParser visitor = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(LongValue value, S context) {
return getBuilder().append(value.getValue() + 1);
}
};
select.accept(new StatementDeParser(visitor, new SelectDeParser(), out), null);
assertEquals("SELECT CONVERT( 43, UNSIGNED )", out.toString());
roundTrip(select, Dialect.MYSQL);
}

@ParameterizedTest
@ValueSource(strings = {"SELECT CONVERT(1,)", "SELECT CONVERT(, SIGNED)",
"SELECT CONVERT(1, SIGNED, 120)", "SELECT CONVERT(1 USING)"})
void rejectsIncompleteArguments(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.MYSQL));
}

private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException {
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect));
}

private static TranscodingFunction conversion(PlainSelect select) {
return assertInstanceOf(TranscodingFunction.class, select.getSelectItem(0).getExpression());
}

private static void roundTrip(PlainSelect select, Dialect dialect) throws Exception {
StringBuilder out = new StringBuilder();
select.accept(new StatementDeParser(out), null);
assertEquals(select.toString(), out.toString());
assertEquals(select.toString(), parse(out.toString(), dialect).toString());
}
}
Loading