Skip to content

Commit 1af2ff3

Browse files
committed
fix: share MySQL account names across CREATE USER and DEFINER
CREATE USER and DEFINER read user@host with their own copies of the same rule. Neither accepted app@'%' or 'app'@localhost, because the lexer reads app@ and @localhost as single tokens. Without a dialect a double-quoted part also lost its quotes, so CREATE USER "app"@"%" rendered as app@%. Add one MySqlAccountName() production that splits those tokens, keep the quote character of double-quoted and backquoted parts apart from their value, and use it for CREATE USER and DEFINER. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent bdf8ac3 commit 1af2ff3

3 files changed

Lines changed: 197 additions & 10 deletions

File tree

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

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15502,11 +15502,66 @@ StringValue MySqlAccountNamePart():
1550215502
(
1550315503
token=<S_CHAR_LITERAL> { value = stringValue(token); }
1550415504
|
15505-
name=RelObjectName() { value = new StringValue(name).setQuoteStr(""); }
15505+
name=RelObjectName()
15506+
{
15507+
// a double-quoted or backquoted part keeps its quote character separately from its
15508+
// value, like a string literal; new StringValue(name) would drop the double quotes
15509+
value = name.length() > 1 && (name.charAt(0) == '"' || name.charAt(0) == '`')
15510+
&& name.charAt(name.length() - 1) == name.charAt(0)
15511+
? new StringValue().withValue(name.substring(1, name.length() - 1))
15512+
.setQuoteStr(name.substring(0, 1))
15513+
: new StringValue().withValue(name).setQuoteStr("");
15514+
}
1550615515
)
1550715516
{ return value; }
1550815517
}
1550915518

15519+
/**
15520+
* A MySQL account name, user[@host], shared by CREATE USER, DEFINER and the account statements.
15521+
* Each part is unquoted or quoted with ', " or `. The lexer reads an unquoted user@host, and the
15522+
* user@ in front of a quoted host, as one identifier, and @host after a quoted user as one
15523+
* at-identifier, so those spellings are split here.
15524+
*/
15525+
UserAccount MySqlAccountName():
15526+
{
15527+
UserAccount account = new UserAccount();
15528+
StringValue part;
15529+
Token token;
15530+
int at;
15531+
}
15532+
{
15533+
(
15534+
// user@ followed by a quoted host
15535+
LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0
15536+
&& getToken(1).image.indexOf('@') == getToken(1).image.length() - 1 })
15537+
token=<S_IDENTIFIER>
15538+
{ account.setUser(new StringValue().withValue(token.image.substring(0, token.image.length() - 1))
15539+
.setQuoteStr("")); }
15540+
part=MySqlAccountNamePart() { account.setHost(part); }
15541+
|
15542+
// unquoted user@host
15543+
LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0
15544+
&& getToken(1).image.indexOf('@') == getToken(1).image.lastIndexOf('@') })
15545+
token=<S_IDENTIFIER>
15546+
{
15547+
at = token.image.indexOf('@');
15548+
account.setUser(new StringValue().withValue(token.image.substring(0, at)).setQuoteStr(""));
15549+
account.setHost(new StringValue().withValue(token.image.substring(at + 1)).setQuoteStr(""));
15550+
}
15551+
|
15552+
part=MySqlAccountNamePart() { account.setUser(part); }
15553+
[
15554+
<K_AT_SIGN> part=MySqlAccountNamePart() { account.setHost(part); }
15555+
|
15556+
// a quoted user followed by an unquoted @host
15557+
LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && !getToken(1).image.startsWith("@@") })
15558+
token=<S_AT_IDENTIFIER>
15559+
{ account.setHost(new StringValue().withValue(token.image.substring(1)).setQuoteStr("")); }
15560+
]
15561+
)
15562+
{ return account; }
15563+
}
15564+
1551015565
UserAuthentication MySqlUserAuthentication():
1551115566
{
1551215567
UserAuthentication authentication = new UserAuthentication();
@@ -15557,14 +15612,11 @@ UserAuthentication MySqlUserAuthentication():
1555715612

1555815613
UserAccount MySqlUserAccount():
1555915614
{
15560-
UserAccount account = new UserAccount();
15561-
StringValue user = null;
15562-
StringValue host = null;
15615+
UserAccount account;
1556315616
UserAuthentication authentication = null;
1556415617
}
1556515618
{
15566-
user=MySqlAccountNamePart() { account.setUser(user); }
15567-
[ <K_AT_SIGN> host=MySqlAccountNamePart() { account.setHost(host); } ]
15619+
account=MySqlAccountName()
1556815620
[ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ]
1556915621
{ return account; }
1557015622
}
@@ -15710,8 +15762,7 @@ AlterEvent AlterEvent():
1571015762
/** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */
1571115763
void MySqlDefiner(MySqlDefiner definer):
1571215764
{
15713-
StringValue user;
15714-
StringValue host;
15765+
UserAccount account;
1571515766
String name;
1571615767
}
1571715768
{
@@ -15721,8 +15772,7 @@ void MySqlDefiner(MySqlDefiner definer):
1572115772
{ definer.setUser(new StringValue(name).setQuoteStr("")); }
1572215773
[ "(" ")" { definer.setCurrentUserParentheses(true); } ]
1572315774
|
15724-
user=MySqlAccountNamePart() { definer.setUser(user); }
15725-
[ <K_AT_SIGN> host=MySqlAccountNamePart() { definer.setHost(host); } ]
15775+
account=MySqlAccountName() { definer.setUser(account.getUser()); definer.setHost(account.getHost()); }
1572615776
)
1572715777
}
1572815778

‎src/site/sphinx/usage.rst‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,20 @@ the PostgreSQL ``CreateRole`` AST for this ambiguous form::
17181718
"CREATE USER app",
17191719
parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL));
17201720

1721+
MySQL account names (``user[@host]``) of ``CREATE USER`` and of view, trigger and
1722+
routine ``DEFINER`` clauses share one grammar rule and are read into separate user
1723+
and host ``StringValue`` parts (``UserAccount`` and ``MySqlDefiner``). Every MySQL
1724+
spelling is accepted, including ``app@localhost``, ``app@'%'`` and
1725+
``'app'@localhost``. A part quoted with ``'``, ``"`` or a backquote keeps its quote
1726+
character in ``getQuoteStr()`` and its text in ``getValue()``::
1727+
1728+
CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse(
1729+
"CREATE USER `app`@'%' IDENTIFIED BY 'secret'");
1730+
UserAccount account = createUser.getAccounts().get(0);
1731+
account.getUser().getValue(); // app
1732+
account.getUser().getQuoteStr(); // `
1733+
account.getHost().getValue(); // %
1734+
17211735
``Grant`` retains its string-based getters, setters and fluent methods. Its
17221736
``PrivilegeClause`` exposes typed ``Privilege`` items, column lists,
17231737
``PrivilegeTarget`` kinds, multiple role memberships and grantor information.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.statement.create.user;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
15+
import static org.junit.jupiter.api.Assertions.assertNull;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
18+
import java.util.function.Consumer;
19+
import net.sf.jsqlparser.JSQLParserException;
20+
import net.sf.jsqlparser.expression.StringValue;
21+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
22+
import net.sf.jsqlparser.parser.CCJSqlParser;
23+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
24+
import net.sf.jsqlparser.statement.MySqlDefiner;
25+
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
26+
import net.sf.jsqlparser.statement.create.view.CreateView;
27+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.Arguments;
30+
import org.junit.jupiter.params.provider.MethodSource;
31+
import org.junit.jupiter.params.provider.ValueSource;
32+
import java.util.stream.Stream;
33+
34+
class MySqlAccountNameTest {
35+
36+
private static final Consumer<CCJSqlParser> MYSQL = p -> p.withDialect(Dialect.MYSQL);
37+
private static final Consumer<CCJSqlParser> NO_DIALECT = p -> {
38+
};
39+
40+
/** SQL spelling, user value, user quote, host value, host quote. */
41+
static Stream<Arguments> accounts() {
42+
return Stream.of(
43+
Arguments.of("app", "app", "", null, null),
44+
Arguments.of("app@localhost", "app", "", "localhost", ""),
45+
Arguments.of("app@'%'", "app", "", "%", "'"),
46+
Arguments.of("app@'10.0.0.%'", "app", "", "10.0.0.%", "'"),
47+
Arguments.of("'app'@localhost", "app", "'", "localhost", ""),
48+
Arguments.of("'app'@'%'", "app", "'", "%", "'"),
49+
Arguments.of("`app`@`%`", "app", "`", "%", "`"),
50+
Arguments.of("\"app\"@\"%\"", "app", "\"", "%", "\""),
51+
Arguments.of("'app'", "app", "'", null, null),
52+
Arguments.of("`my app`@'%'", "my app", "`", "%", "'"));
53+
}
54+
55+
@ParameterizedTest
56+
@MethodSource("accounts")
57+
void createUserSplitsEveryAccountSpelling(String account, String user, String userQuote,
58+
String host, String hostQuote) throws JSQLParserException {
59+
for (Consumer<CCJSqlParser> mode : java.util.Arrays.asList(MYSQL, NO_DIALECT)) {
60+
String sql = "CREATE USER " + account + " IDENTIFIED BY 'x'";
61+
CreateUser createUser = assertInstanceOf(CreateUser.class,
62+
assertSqlCanBeParsedAndDeparsed(sql, true, mode));
63+
assertEquals(sql, createUser.toString());
64+
assertEquals(sql, deparse(createUser));
65+
assertPart(createUser.getAccounts().get(0).getUser(), user, userQuote);
66+
assertPart(createUser.getAccounts().get(0).getHost(), host, hostQuote);
67+
}
68+
}
69+
70+
@ParameterizedTest
71+
@MethodSource("accounts")
72+
void definerSplitsEveryAccountSpelling(String account, String user, String userQuote,
73+
String host, String hostQuote) throws JSQLParserException {
74+
String view = "CREATE DEFINER = " + account + " VIEW v AS SELECT 1";
75+
CreateView createView = assertInstanceOf(CreateView.class,
76+
assertSqlCanBeParsedAndDeparsed(view, true, MYSQL));
77+
MySqlDefiner definer = createView.getMySqlOptions().getDefiner();
78+
assertPart(definer.getUser(), user, userQuote);
79+
assertPart(definer.getHost(), host, hostQuote);
80+
81+
String trigger = "CREATE DEFINER = " + account
82+
+ " TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET NEW.a = 1";
83+
CreateTrigger createTrigger = assertInstanceOf(CreateTrigger.class,
84+
assertSqlCanBeParsedAndDeparsed(trigger, true, MYSQL));
85+
assertEquals(trigger, createTrigger.toString());
86+
}
87+
88+
@ParameterizedTest
89+
@ValueSource(strings = {"CREATE USER app@", "CREATE USER @host", "CREATE USER 'app'@"})
90+
void rejectsMalformedAccounts(String sql) {
91+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL));
92+
}
93+
94+
@org.junit.jupiter.api.Test
95+
void keepsDoubleQuotesWithoutDialect() throws JSQLParserException {
96+
// before, the double quotes were dropped and the account rendered as app@%
97+
String sql = "CREATE USER \"app\"@\"%\" IDENTIFIED BY 'x'";
98+
assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
99+
assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString());
100+
}
101+
102+
@org.junit.jupiter.api.Test
103+
void definerCurrentUserIsUnchanged() throws JSQLParserException {
104+
CreateView view = assertInstanceOf(CreateView.class, assertSqlCanBeParsedAndDeparsed(
105+
"CREATE DEFINER = CURRENT_USER() VIEW v AS SELECT 1", true, MYSQL));
106+
assertNull(view.getMySqlOptions().getDefiner().getHost());
107+
}
108+
109+
private static void assertPart(StringValue part, String value, String quote) {
110+
if (value == null) {
111+
assertNull(part);
112+
return;
113+
}
114+
assertEquals(value, part.getValue());
115+
assertEquals(quote, part.getQuoteStr());
116+
}
117+
118+
private static String deparse(CreateUser createUser) {
119+
StringBuilder builder = new StringBuilder();
120+
createUser.accept(new StatementDeParser(builder), null);
121+
return builder.toString();
122+
}
123+
}

0 commit comments

Comments
 (0)