Skip to content
Open
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
70 changes: 60 additions & 10 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -15502,11 +15502,66 @@ StringValue MySqlAccountNamePart():
(
token=<S_CHAR_LITERAL> { value = stringValue(token); }
|
name=RelObjectName() { value = new StringValue(name).setQuoteStr(""); }
name=RelObjectName()
{
// a double-quoted or backquoted part keeps its quote character separately from its
// value, like a string literal; new StringValue(name) would drop the double quotes
value = name.length() > 1 && (name.charAt(0) == '"' || name.charAt(0) == '`')
&& name.charAt(name.length() - 1) == name.charAt(0)
? new StringValue().withValue(name.substring(1, name.length() - 1))
.setQuoteStr(name.substring(0, 1))
: new StringValue().withValue(name).setQuoteStr("");
}
)
{ return value; }
}

/**
* A MySQL account name, user[@host], shared by CREATE USER, DEFINER and the account statements.
* Each part is unquoted or quoted with ', " or `. The lexer reads an unquoted user@host, and the
* user@ in front of a quoted host, as one identifier, and @host after a quoted user as one
* at-identifier, so those spellings are split here.
*/
UserAccount MySqlAccountName():
{
UserAccount account = new UserAccount();
StringValue part;
Token token;
int at;
}
{
(
// user@ followed by a quoted host
LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0
&& getToken(1).image.indexOf('@') == getToken(1).image.length() - 1 })
token=<S_IDENTIFIER>
{ account.setUser(new StringValue().withValue(token.image.substring(0, token.image.length() - 1))
.setQuoteStr("")); }
part=MySqlAccountNamePart() { account.setHost(part); }
|
// unquoted user@host
LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0
&& getToken(1).image.indexOf('@') == getToken(1).image.lastIndexOf('@') })
token=<S_IDENTIFIER>
{
at = token.image.indexOf('@');
account.setUser(new StringValue().withValue(token.image.substring(0, at)).setQuoteStr(""));
account.setHost(new StringValue().withValue(token.image.substring(at + 1)).setQuoteStr(""));
}
|
part=MySqlAccountNamePart() { account.setUser(part); }
[
<K_AT_SIGN> part=MySqlAccountNamePart() { account.setHost(part); }
|
// a quoted user followed by an unquoted @host
LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && !getToken(1).image.startsWith("@@") })
token=<S_AT_IDENTIFIER>
{ account.setHost(new StringValue().withValue(token.image.substring(1)).setQuoteStr("")); }
]
)
{ return account; }
}

UserAuthentication MySqlUserAuthentication():
{
UserAuthentication authentication = new UserAuthentication();
Expand Down Expand Up @@ -15557,14 +15612,11 @@ UserAuthentication MySqlUserAuthentication():

UserAccount MySqlUserAccount():
{
UserAccount account = new UserAccount();
StringValue user = null;
StringValue host = null;
UserAccount account;
UserAuthentication authentication = null;
}
{
user=MySqlAccountNamePart() { account.setUser(user); }
[ <K_AT_SIGN> host=MySqlAccountNamePart() { account.setHost(host); } ]
account=MySqlAccountName()
[ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ]
{ return account; }
}
Expand Down Expand Up @@ -15710,8 +15762,7 @@ AlterEvent AlterEvent():
/** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */
void MySqlDefiner(MySqlDefiner definer):
{
StringValue user;
StringValue host;
UserAccount account;
String name;
}
{
Expand All @@ -15721,8 +15772,7 @@ void MySqlDefiner(MySqlDefiner definer):
{ definer.setUser(new StringValue(name).setQuoteStr("")); }
[ "(" ")" { definer.setCurrentUserParentheses(true); } ]
|
user=MySqlAccountNamePart() { definer.setUser(user); }
[ <K_AT_SIGN> host=MySqlAccountNamePart() { definer.setHost(host); } ]
account=MySqlAccountName() { definer.setUser(account.getUser()); definer.setHost(account.getHost()); }
)
}

Expand Down
14 changes: 14 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,20 @@ the PostgreSQL ``CreateRole`` AST for this ambiguous form::
"CREATE USER app",
parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL));

MySQL account names (``user[@host]``) of ``CREATE USER`` and of view, trigger and
routine ``DEFINER`` clauses share one grammar rule and are read into separate user
and host ``StringValue`` parts (``UserAccount`` and ``MySqlDefiner``). Every MySQL
spelling is accepted, including ``app@localhost``, ``app@'%'`` and
``'app'@localhost``. A part quoted with ``'``, ``"`` or a backquote keeps its quote
character in ``getQuoteStr()`` and its text in ``getValue()``::

CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse(
"CREATE USER `app`@'%' IDENTIFIED BY 'secret'");
UserAccount account = createUser.getAccounts().get(0);
account.getUser().getValue(); // app
account.getUser().getQuoteStr(); // `
account.getHost().getValue(); // %

``Grant`` retains its string-based getters, setters and fluent methods. Its
``PrivilegeClause`` exposes typed ``Privilege`` items, column lists,
``PrivilegeTarget`` kinds, multiple role memberships and grantor information.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*-
* #%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.statement.create.user;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.function.Consumer;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParser;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.MySqlDefiner;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
import net.sf.jsqlparser.statement.create.view.CreateView;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.stream.Stream;

class MySqlAccountNameTest {

private static final Consumer<CCJSqlParser> MYSQL = p -> p.withDialect(Dialect.MYSQL);
private static final Consumer<CCJSqlParser> NO_DIALECT = p -> {
};

/** SQL spelling, user value, user quote, host value, host quote. */
static Stream<Arguments> accounts() {
return Stream.of(
Arguments.of("app", "app", "", null, null),
Arguments.of("app@localhost", "app", "", "localhost", ""),
Arguments.of("app@'%'", "app", "", "%", "'"),
Arguments.of("app@'10.0.0.%'", "app", "", "10.0.0.%", "'"),
Arguments.of("'app'@localhost", "app", "'", "localhost", ""),
Arguments.of("'app'@'%'", "app", "'", "%", "'"),
Arguments.of("`app`@`%`", "app", "`", "%", "`"),
Arguments.of("\"app\"@\"%\"", "app", "\"", "%", "\""),
Arguments.of("'app'", "app", "'", null, null),
Arguments.of("`my app`@'%'", "my app", "`", "%", "'"));
}

@ParameterizedTest
@MethodSource("accounts")
void createUserSplitsEveryAccountSpelling(String account, String user, String userQuote,
String host, String hostQuote) throws JSQLParserException {
for (Consumer<CCJSqlParser> mode : java.util.Arrays.asList(MYSQL, NO_DIALECT)) {
String sql = "CREATE USER " + account + " IDENTIFIED BY 'x'";
CreateUser createUser = assertInstanceOf(CreateUser.class,
assertSqlCanBeParsedAndDeparsed(sql, true, mode));
assertEquals(sql, createUser.toString());
assertEquals(sql, deparse(createUser));
assertPart(createUser.getAccounts().get(0).getUser(), user, userQuote);
assertPart(createUser.getAccounts().get(0).getHost(), host, hostQuote);
}
}

@ParameterizedTest
@MethodSource("accounts")
void definerSplitsEveryAccountSpelling(String account, String user, String userQuote,
String host, String hostQuote) throws JSQLParserException {
String view = "CREATE DEFINER = " + account + " VIEW v AS SELECT 1";
CreateView createView = assertInstanceOf(CreateView.class,
assertSqlCanBeParsedAndDeparsed(view, true, MYSQL));
MySqlDefiner definer = createView.getMySqlOptions().getDefiner();
assertPart(definer.getUser(), user, userQuote);
assertPart(definer.getHost(), host, hostQuote);

String trigger = "CREATE DEFINER = " + account
+ " TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET NEW.a = 1";
CreateTrigger createTrigger = assertInstanceOf(CreateTrigger.class,
assertSqlCanBeParsedAndDeparsed(trigger, true, MYSQL));
assertEquals(trigger, createTrigger.toString());
}

@ParameterizedTest
@ValueSource(strings = {"CREATE USER app@", "CREATE USER @host", "CREATE USER 'app'@"})
void rejectsMalformedAccounts(String sql) {
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL));
}

@org.junit.jupiter.api.Test
void keepsDoubleQuotesWithoutDialect() throws JSQLParserException {
// before, the double quotes were dropped and the account rendered as app@%
String sql = "CREATE USER \"app\"@\"%\" IDENTIFIED BY 'x'";
assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString());
}

@org.junit.jupiter.api.Test
void definerCurrentUserIsUnchanged() throws JSQLParserException {
CreateView view = assertInstanceOf(CreateView.class, assertSqlCanBeParsedAndDeparsed(
"CREATE DEFINER = CURRENT_USER() VIEW v AS SELECT 1", true, MYSQL));
assertNull(view.getMySqlOptions().getDefiner().getHost());
}

private static void assertPart(StringValue part, String value, String quote) {
if (value == null) {
assertNull(part);
return;
}
assertEquals(value, part.getValue());
assertEquals(quote, part.getQuoteStr());
}

private static String deparse(CreateUser createUser) {
StringBuilder builder = new StringBuilder();
createUser.accept(new StatementDeParser(builder), null);
return builder.toString();
}
}
Loading