From 539e7bb22c6a234bf76c5e22fa3a1fc3ed2aec66 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 4 Sep 2026 18:49:55 +0900 Subject: [PATCH] feat(parser): model MySQL CREATE USER --- src/main/java/module-info.java | 1 + .../statement/StatementVisitor.java | 7 ++ .../statement/StatementVisitorAdapter.java | 6 ++ .../statement/create/user/CreateUser.java | 70 ++++++++++++++ .../statement/create/user/UserAccount.java | 72 ++++++++++++++ .../create/user/UserAuthentication.java | 84 ++++++++++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 11 +++ .../util/deparser/StatementDeParser.java | 7 ++ .../validator/StatementValidator.java | 10 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 95 +++++++++++++++++++ .../statement/create/user/CreateUserTest.java | 71 ++++++++++++++ 11 files changed, 434 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/user/CreateUser.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/user/UserAccount.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/user/UserAuthentication.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ada4bfdf45..b3b3ed03f3 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -33,6 +33,7 @@ exports net.sf.jsqlparser.statement.create.sequence; exports net.sf.jsqlparser.statement.create.synonym; exports net.sf.jsqlparser.statement.create.table; + exports net.sf.jsqlparser.statement.create.user; exports net.sf.jsqlparser.statement.create.view; exports net.sf.jsqlparser.statement.delete; exports net.sf.jsqlparser.statement.drop; diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 24a5ed6abc..823b8271f2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -23,6 +23,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -125,6 +126,12 @@ default void visit(CreateDatabase createDatabase) { this.visit(createDatabase, null); } + T visit(CreateUser createUser, S context); + + default void visit(CreateUser createUser) { + this.visit(createUser, null); + } + T visit(CreateTable createTable, S context); default void visit(CreateTable createTable) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 4193645b45..a8abb46eea 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -27,6 +27,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -267,6 +268,11 @@ public T visit(CreateDatabase createDatabase, S context) { return null; } + @Override + public T visit(CreateUser createUser, S context) { + return null; + } + @Override public T visit(CreateTable createTable, S context) { return createTable.getTable().accept(fromItemVisitor, context); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/user/CreateUser.java b/src/main/java/net/sf/jsqlparser/statement/create/user/CreateUser.java new file mode 100644 index 0000000000..ce5f16273c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/user/CreateUser.java @@ -0,0 +1,70 @@ +/*- + * #%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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** MySQL {@code CREATE USER} statement with structured account and authentication data. */ +public class CreateUser implements Statement { + + private boolean ifNotExists; + private final List accounts = new ArrayList<>(); + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public List getAccounts() { + return Collections.unmodifiableList(accounts); + } + + public void setAccounts(Collection accounts) { + this.accounts.clear(); + if (accounts != null) { + this.accounts.addAll(accounts); + } + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + @Override + public String toString() { + return "CREATE USER " + (ifNotExists ? "IF NOT EXISTS " : "") + + accounts.stream().map(Object::toString).collect(Collectors.joining(", ")); + } + + public CreateUser withIfNotExists(boolean ifNotExists) { + setIfNotExists(ifNotExists); + return this; + } + + public CreateUser withAccounts(Collection accounts) { + setAccounts(accounts); + return this; + } + + public CreateUser addAccounts(UserAccount... accounts) { + Collections.addAll(this.accounts, accounts); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/user/UserAccount.java b/src/main/java/net/sf/jsqlparser/statement/create/user/UserAccount.java new file mode 100644 index 0000000000..e149cb4b77 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/user/UserAccount.java @@ -0,0 +1,72 @@ +/*- + * #%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 java.io.Serializable; +import net.sf.jsqlparser.expression.StringValue; + +/** A user and optional host together with its authentication clause. */ +public class UserAccount implements Serializable { + + private StringValue user; + private StringValue host; + private UserAuthentication authentication; + + public StringValue getUser() { + return user; + } + + public void setUser(StringValue user) { + this.user = user; + } + + public StringValue getHost() { + return host; + } + + public void setHost(StringValue host) { + this.host = host; + } + + public UserAuthentication getAuthentication() { + return authentication; + } + + public void setAuthentication(UserAuthentication authentication) { + this.authentication = authentication; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder().append(user); + if (host != null) { + builder.append("@").append(host); + } + if (authentication != null) { + builder.append(" ").append(authentication); + } + return builder.toString(); + } + + public UserAccount withUser(StringValue user) { + setUser(user); + return this; + } + + public UserAccount withHost(StringValue host) { + setHost(host); + return this; + } + + public UserAccount withAuthentication(UserAuthentication authentication) { + setAuthentication(authentication); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/user/UserAuthentication.java b/src/main/java/net/sf/jsqlparser/statement/create/user/UserAuthentication.java new file mode 100644 index 0000000000..d05e76af91 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/user/UserAuthentication.java @@ -0,0 +1,84 @@ +/*- + * #%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 java.io.Serializable; +import net.sf.jsqlparser.expression.StringValue; + +/** Structured authentication clause of a MySQL user account. */ +public class UserAuthentication implements Serializable { + + public enum Mode { + BY_PASSWORD, BY_RANDOM_PASSWORD, WITH_PLUGIN, WITH_PLUGIN_BY_PASSWORD, WITH_PLUGIN_BY_RANDOM_PASSWORD, WITH_PLUGIN_AS_STRING + } + + private Mode mode; + private String plugin; + private StringValue credential; + + public Mode getMode() { + return mode; + } + + public void setMode(Mode mode) { + this.mode = mode; + } + + public String getPlugin() { + return plugin; + } + + public void setPlugin(String plugin) { + this.plugin = plugin; + } + + public StringValue getCredential() { + return credential; + } + + public void setCredential(StringValue credential) { + this.credential = credential; + } + + @Override + public String toString() { + switch (mode) { + case BY_PASSWORD: + return "IDENTIFIED BY " + credential; + case BY_RANDOM_PASSWORD: + return "IDENTIFIED BY RANDOM PASSWORD"; + case WITH_PLUGIN: + return "IDENTIFIED WITH " + plugin; + case WITH_PLUGIN_BY_PASSWORD: + return "IDENTIFIED WITH " + plugin + " BY " + credential; + case WITH_PLUGIN_BY_RANDOM_PASSWORD: + return "IDENTIFIED WITH " + plugin + " BY RANDOM PASSWORD"; + case WITH_PLUGIN_AS_STRING: + return "IDENTIFIED WITH " + plugin + " AS " + credential; + default: + throw new IllegalStateException("Unsupported authentication mode: " + mode); + } + } + + public UserAuthentication withMode(Mode mode) { + setMode(mode); + return this; + } + + public UserAuthentication withPlugin(String plugin) { + setPlugin(plugin); + return this; + } + + public UserAuthentication withCredential(StringValue credential) { + setCredential(credential); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index d78884afd6..7ac65f4b59 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -104,6 +104,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -1555,6 +1556,16 @@ public void visit(CreateDatabase createDatabase) { StatementVisitor.super.visit(createDatabase); } + @Override + public Void visit(CreateUser createUser, S context) { + return null; + } + + @Override + public void visit(CreateUser createUser) { + StatementVisitor.super.visit(createUser); + } + @Override public Void visit(CreateTable create, S context) { visit(create.getTable(), null); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index bd256c02ed..34eb53dfdc 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -48,6 +48,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -437,6 +438,12 @@ public StringBuilder visit(CreateDatabase aThis, S context) { return builder; } + @Override + public StringBuilder visit(CreateUser createUser, S context) { + builder.append(createUser); + return builder; + } + @Override public StringBuilder visit(CreateSequence createSequence, S context) { new CreateSequenceDeParser(builder).deParse(createSequence); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index a3debcba3e..2d4fd3e0b4 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -46,6 +46,7 @@ import net.sf.jsqlparser.statement.create.sequence.CreateSequence; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.user.CreateUser; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.delete.Delete; @@ -307,6 +308,11 @@ public Void visit(CreateDatabase aThis, S context) { return null; } + @Override + public Void visit(CreateUser createUser, S context) { + return null; + } + @Override public Void visit(CreateSequence createSequence, S context) { getValidator(CreateSequenceValidator.class).validate(createSequence); @@ -539,6 +545,10 @@ public void visit(CreateSchema aThis) { visit(aThis, null); } + public void visit(CreateUser createUser) { + visit(createUser, null); + } + public void visit(CreateSequence createSequence) { visit(createSequence, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a4de9580b3..3745afc270 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -56,6 +56,7 @@ import net.sf.jsqlparser.statement.create.schema.*; import net.sf.jsqlparser.statement.create.synonym.*; import net.sf.jsqlparser.statement.create.sequence.*; import net.sf.jsqlparser.statement.create.table.*; +import net.sf.jsqlparser.statement.create.user.*; import net.sf.jsqlparser.statement.create.view.*; import net.sf.jsqlparser.statement.delete.*; import net.sf.jsqlparser.statement.drop.*; @@ -1760,6 +1761,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1772,6 +1774,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -11531,6 +11534,96 @@ ColumnOption ColumnDefinitionOption(): { { return option; } } +StringValue MySqlAccountNamePart(): +{ + Token token = null; + String name = null; + StringValue value = null; +} +{ + ( + token= { value = new StringValue(token.image); } + | + name=RelObjectName() { value = new StringValue(name).setQuoteStr(""); } + ) + { return value; } +} + +UserAuthentication MySqlUserAuthentication(): +{ + UserAuthentication authentication = new UserAuthentication(); + String plugin = null; + Token credential = null; +} +{ + + ( + + ( + credential= { + authentication.setMode(UserAuthentication.Mode.BY_PASSWORD); + authentication.setCredential(new StringValue(credential.image)); + } + | + { + authentication.setMode(UserAuthentication.Mode.BY_RANDOM_PASSWORD); + } + ) + | + plugin=RelObjectName() { + authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN); + authentication.setPlugin(plugin); + } + [ + + ( + credential= { + authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_BY_PASSWORD); + authentication.setCredential(new StringValue(credential.image)); + } + | + { + authentication.setMode( + UserAuthentication.Mode.WITH_PLUGIN_BY_RANDOM_PASSWORD); + } + ) + | + credential= { + authentication.setMode(UserAuthentication.Mode.WITH_PLUGIN_AS_STRING); + authentication.setCredential(new StringValue(credential.image)); + } + ] + ) + { return authentication; } +} + +UserAccount MySqlUserAccount(): +{ + UserAccount account = new UserAccount(); + StringValue user = null; + StringValue host = null; + UserAuthentication authentication = null; +} +{ + user=MySqlAccountNamePart() { account.setUser(user); } + [ host=MySqlAccountNamePart() { account.setHost(host); } ] + [ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ] + { return account; } +} + +CreateUser CreateUser(): +{ + CreateUser createUser = new CreateUser(); + UserAccount account = null; +} +{ + + [ LOOKAHEAD(3) { createUser.setIfNotExists(true); } ] + account=MySqlUserAccount() { createUser.addAccounts(account); } + ( "," account=MySqlUserAccount() { createUser.addAccounts(account); } )* + { return createUser; } +} + CreateSchema CreateSchema(): { Token tk = null; @@ -14719,6 +14812,8 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + statement = CreateUser() + | statement = CreateFunctionStatement(isUsingOrReplace) | statement = CreateSchema() diff --git a/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java b/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java new file mode 100644 index 0000000000..d0dcbb1af9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/user/CreateUserTest.java @@ -0,0 +1,71 @@ +/*- + * #%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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; + +class CreateUserTest { + + @Test + void parsesAccountAndPasswordAsStructuredValues() throws JSQLParserException { + String sql = "CREATE USER IF NOT EXISTS 'replicator' IDENTIFIED BY 'replpass'"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, CCJSqlParserUtil.parse(sql)); + + assertTrue(createUser.isIfNotExists()); + assertEquals(1, createUser.getAccounts().size()); + UserAccount account = createUser.getAccounts().get(0); + assertEquals("replicator", account.getUser().getValue()); + assertNull(account.getHost()); + assertEquals(UserAuthentication.Mode.BY_PASSWORD, account.getAuthentication().getMode()); + assertEquals("replpass", account.getAuthentication().getCredential().getValue()); + assertSqlCanBeParsedAndDeparsed(sql); + } + + @Test + void parsesHostAndOptionalAuthentication() throws JSQLParserException { + String sql = "CREATE USER IF NOT EXISTS 'snapshot'@'%'"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, assertSqlCanBeParsedAndDeparsed(sql)); + + UserAccount account = createUser.getAccounts().get(0); + assertEquals("snapshot", account.getUser().getValue()); + assertEquals("%", account.getHost().getValue()); + assertNull(account.getAuthentication()); + } + + @Test + void parsesMultipleAccountsAndAuthenticationPlugin() throws JSQLParserException { + String sql = "CREATE USER alice@localhost IDENTIFIED WITH caching_sha2_password " + + "BY 'secret', 'service'@'10.%' IDENTIFIED BY RANDOM PASSWORD"; + CreateUser createUser = + assertInstanceOf(CreateUser.class, assertSqlCanBeParsedAndDeparsed(sql)); + + assertFalse(createUser.isIfNotExists()); + assertEquals(2, createUser.getAccounts().size()); + UserAuthentication pluginAuthentication = + createUser.getAccounts().get(0).getAuthentication(); + assertEquals(UserAuthentication.Mode.WITH_PLUGIN_BY_PASSWORD, + pluginAuthentication.getMode()); + assertEquals("caching_sha2_password", pluginAuthentication.getPlugin()); + assertEquals("secret", pluginAuthentication.getCredential().getValue()); + assertEquals(UserAuthentication.Mode.BY_RANDOM_PASSWORD, + createUser.getAccounts().get(1).getAuthentication().getMode()); + } +}