diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 974c93baf..5c431829c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -301,6 +301,19 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Whether the next two tokens form a character-set-introduced string literal, + * for example {@code _utf8mb4'text'} or {@code _ISO8859_1 'text'}. + */ + private boolean isCharacterSetIntroducedStringAhead() { + Token introducer = getToken(1); + Token literal = getToken(2); + return introducer.kind == S_IDENTIFIER + && introducer.image != null + && introducer.image.length() > 1 + && introducer.image.charAt(0) == '_' + && literal.kind == S_CHAR_LITERAL; + } /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. @@ -8067,6 +8080,18 @@ Expression SimpleExpression(): } } +StringValue CharacterSetIntroducedString(): +{ + Token introducer; + Token literal; +} +{ + introducer= literal= + { + return new StringValue(literal.image).withPrefix(introducer.image); + } +} + /** Multiplicative/bitwise-shift — higher precedence */ Expression MulChain(): { @@ -8243,6 +8268,9 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2, {!interrupted}) { retval = new AllValue(); } + | LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() }) + retval=CharacterSetIntroducedString() + | LOOKAHEAD(2, {!interrupted}) retval=Column() [ LOOKAHEAD( "(" "+" ")" ("," | ")") ) diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index a0f748937..a385e17bb 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -36,6 +36,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.AllValue; import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.CollateExpression; import net.sf.jsqlparser.expression.DoubleValue; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; @@ -4183,6 +4184,38 @@ public void testEscaped() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT _utf8'testvalue'"); } + @ParameterizedTest + @ValueSource(strings = {"_utf8mb4'testvalue'", "_utf8mb4 'testvalue'", + "_utf8 'testvalue'", "_latin1'testvalue'", "_binary'testvalue'", + "_ISO8859_1 'testvalue'"}) + public void testCharacterSetIntroducedString(String expression) throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT " + expression); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + StringValue stringValue = assertInstanceOf(StringValue.class, selectItem.getExpression()); + + assertEquals(expression.substring(0, expression.indexOf('\'')).trim(), + stringValue.getPrefix()); + assertEquals("testvalue", stringValue.getValue()); + assertNull(selectItem.getAlias()); + } + + @Test + public void testCharacterSetIntroducedStringWithCollate() throws JSQLParserException { + String sql = "SELECT _utf8mb4'some text' COLLATE utf8mb4_unicode_ci AS custom_string"; + Select select = (Select) CCJSqlParserUtil.parse(sql); + SelectItem selectItem = select.getPlainSelect().getSelectItem(0); + CollateExpression collateExpression = + assertInstanceOf(CollateExpression.class, selectItem.getExpression()); + StringValue stringValue = assertInstanceOf(StringValue.class, + collateExpression.getLeftExpression()); + + assertEquals("_utf8mb4", stringValue.getPrefix()); + assertEquals("some text", stringValue.getValue()); + assertEquals("utf8mb4_unicode_ci", collateExpression.getCollate()); + assertEquals("custom_string", selectItem.getAlias().getName()); + assertEquals(sql, select.toString()); + } + @Test public void testIssue563MultiSubJoin() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(