diff --git a/src/main/java/net/sf/jsqlparser/statement/MySqlDefiner.java b/src/main/java/net/sf/jsqlparser/statement/MySqlDefiner.java new file mode 100644 index 000000000..be8fe0ecf --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/MySqlDefiner.java @@ -0,0 +1,69 @@ +/*- + * #%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; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.StringValue; + +/** Structured account shared by MySQL stored-object {@code DEFINER} clause. */ +public class MySqlDefiner implements Serializable { + + private StringValue user; + private StringValue host; + private boolean currentUserParentheses; + + public boolean isCurrentUserParentheses() { + return currentUserParentheses; + } + + public void setCurrentUserParentheses(boolean currentUserParentheses) { + this.currentUserParentheses = currentUserParentheses; + } + + + public StringValue getUser() { + return user; + } + + public void setUser(StringValue user) { + this.user = user; + currentUserParentheses = false; + } + + public StringValue getHost() { + return host; + } + + public void setHost(StringValue host) { + this.host = host; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder().append(user); + if (currentUserParentheses) { + builder.append("()"); + } + if (host != null) { + builder.append("@").append(host); + } + return builder.toString(); + } + + public MySqlDefiner withUser(StringValue user) { + setUser(user); + return this; + } + + public MySqlDefiner withHost(StringValue host) { + setHost(host); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java index a4ba3e14e..9f95c53ea 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerDefiner.java @@ -9,45 +9,18 @@ */ package net.sf.jsqlparser.statement.create.trigger; -import java.io.Serializable; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.MySqlDefiner; -/** Structured account used by a MySQL trigger {@code DEFINER} clause. */ -public class TriggerDefiner implements Serializable { - - private StringValue user; - private StringValue host; - - 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; - } - +/** Retains the trigger API while sharing MySQL DEFINER account handling with views. */ +public class TriggerDefiner extends MySqlDefiner { @Override - public String toString() { - StringBuilder builder = new StringBuilder().append(user); - if (host != null) { - builder.append("@").append(host); - } - return builder.toString(); - } - public TriggerDefiner withUser(StringValue user) { setUser(user); return this; } + @Override public TriggerDefiner withHost(StringValue host) { setHost(host); return this; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/view/AlterView.java b/src/main/java/net/sf/jsqlparser/statement/create/view/AlterView.java index 079f70bd3..1ede9e8f1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/view/AlterView.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/view/AlterView.java @@ -23,6 +23,36 @@ public class AlterView implements Statement { + private MySqlViewOptions mySqlOptions; + + public MySqlViewOptions getMySqlOptions() { + return mySqlOptions; + } + + public void setMySqlOptions(MySqlViewOptions mySqlOptions) { + this.mySqlOptions = mySqlOptions; + } + + public void appendMySqlOptionsTo(StringBuilder builder) { + if (mySqlOptions != null) { + mySqlOptions.appendTo(builder); + } + } + + private CreateView.CheckOption checkOption; + + public CreateView.CheckOption getCheckOption() { + return checkOption; + } + + public void setCheckOption(CreateView.CheckOption checkOption) { + this.checkOption = checkOption; + } + + public void appendCheckOptionTo(StringBuilder sql) { + CreateView.appendCheckOptionTo(sql, checkOption); + } + private Table view; private Select select; private boolean useReplace = false; @@ -68,12 +98,14 @@ public String toString() { } else { sql = new StringBuilder("ALTER "); } + appendMySqlOptionsTo(sql); sql.append("VIEW "); sql.append(view); if (columnNames != null) { sql.append(PlainSelect.getStringList(columnNames, true, true)); } sql.append(" AS ").append(select); + appendCheckOptionTo(sql); return sql.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java b/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java index 3747023fd..7f5febe15 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java @@ -23,6 +23,22 @@ public class CreateView implements Statement { + private MySqlViewOptions mySqlOptions; + + public MySqlViewOptions getMySqlOptions() { + return mySqlOptions; + } + + public void setMySqlOptions(MySqlViewOptions mySqlOptions) { + this.mySqlOptions = mySqlOptions; + } + + public void appendMySqlOptionsTo(StringBuilder builder) { + if (mySqlOptions != null) { + mySqlOptions.appendTo(builder); + } + } + private Table view; private Select select; private boolean orReplace = false; @@ -271,6 +287,7 @@ public String toString() { if (isOrReplace()) { sql.append("OR REPLACE "); } + appendMySqlOptionsTo(sql); appendForceOptionIfApplicable(sql); if (secure) { sql.append("SECURE "); @@ -328,7 +345,7 @@ public void appendParametersTo(StringBuilder sql) { } } - public void appendOptionsAfterQueryTo(StringBuilder sql) { + public static void appendCheckOptionTo(StringBuilder sql, CheckOption checkOption) { if (checkOption != null) { sql.append(" WITH "); if (checkOption != CheckOption.DEFAULT) { @@ -336,6 +353,10 @@ public void appendOptionsAfterQueryTo(StringBuilder sql) { } sql.append("CHECK OPTION"); } + } + + public void appendOptionsAfterQueryTo(StringBuilder sql) { + appendCheckOptionTo(sql, checkOption); if (withData != null) { sql.append(withData ? " WITH DATA" : " WITH NO DATA"); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/view/MySqlViewOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/view/MySqlViewOptions.java new file mode 100644 index 000000000..e828ddf3f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/view/MySqlViewOptions.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.view; + +import java.io.Serializable; +import net.sf.jsqlparser.statement.MySqlDefiner; + +/** Prefix options shared by MySQL CREATE VIEW and ALTER VIEW. */ +public class MySqlViewOptions implements Serializable { + public enum Algorithm { + UNDEFINED, MERGE, TEMPTABLE + } + + public enum Security { + DEFINER, INVOKER + } + + private Algorithm algorithm; + private MySqlDefiner definer; + private Security security; + + public Algorithm getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(Algorithm algorithm) { + this.algorithm = algorithm; + } + + public MySqlDefiner getDefiner() { + return definer; + } + + public void setDefiner(MySqlDefiner definer) { + this.definer = definer; + } + + public Security getSecurity() { + return security; + } + + public void setSecurity(Security security) { + this.security = security; + } + + public void appendTo(StringBuilder builder) { + if (algorithm != null) { + builder.append("ALGORITHM = ").append(algorithm).append(' '); + } + if (definer != null) { + builder.append("DEFINER = ").append(definer).append(' '); + } + if (security != null) { + builder.append("SQL SECURITY ").append(security).append(' '); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql); + return sql.toString().trim(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 798234f9a..37478412a 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1947,7 +1947,10 @@ public void visit(TableFunction tableFunction) { @Override public Void visit(AlterView alterView, S context) { - throwUnsupported(alterView); + visit(alterView.getView(), context); + if (alterView.getSelect() != null) { + alterView.getSelect().accept((SelectVisitor) this, context); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java index 3c1c0bbaa..514f6f6a0 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterViewDeParser.java @@ -37,6 +37,7 @@ public void deParse(AlterView alterView) { } else { builder.append("ALTER "); } + alterView.appendMySqlOptionsTo(builder); builder.append("VIEW ").append(alterView.getView().getFullyQualifiedName()); if (alterView.getColumnNames() != null) { builder.append(PlainSelect.getStringList(alterView.getColumnNames(), true, true)); @@ -44,6 +45,7 @@ public void deParse(AlterView alterView) { builder.append(" AS "); alterView.getSelect().accept(selectVisitor, null); + alterView.appendCheckOptionTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java index 432ef52b9..e2c34e531 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java @@ -41,6 +41,7 @@ public void deParse(CreateView createView) { if (createView.isOrReplace()) { builder.append("OR REPLACE "); } + createView.appendMySqlOptionsTo(builder); switch (createView.getForce()) { case FORCE: builder.append("FORCE "); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..43b26afbe 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14491,18 +14491,53 @@ AlterEvent AlterEvent(): { return alterEvent; } } +/** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */ +void MySqlDefiner(MySqlDefiner definer): +{ + StringValue user; + StringValue host; + String name; +} +{ + ContextualKeyword("DEFINER") "=" + ( + LOOKAHEAD({ isKeywordAhead("CURRENT_USER") }) name=RelObjectName() + { definer.setUser(new StringValue(name).setQuoteStr("")); } + [ "(" ")" { definer.setCurrentUserParentheses(true); } ] + | + user=MySqlAccountNamePart() { definer.setUser(user); } + [ host=MySqlAccountNamePart() { definer.setHost(host); } ] + ) +} + TriggerDefiner MySqlTriggerDefiner(): +{ TriggerDefiner definer = new TriggerDefiner(); } +{ MySqlDefiner(definer) { return definer; } } + +/** MySQL fixes the prefix order: ALGORITHM, DEFINER, SQL SECURITY. */ +MySqlViewOptions MySqlViewPrefix(): { - TriggerDefiner definer = new TriggerDefiner(); - StringValue user = null; - StringValue host = null; + MySqlViewOptions options = new MySqlViewOptions(); + MySqlDefiner definer; + Token token; + boolean present = false; } { - LOOKAHEAD({ "DEFINER".equalsIgnoreCase(getToken(1).image) }) - "=" user=MySqlAccountNamePart() { definer.setUser(user); } - [ LOOKAHEAD({ getToken(1).kind == K_AT_SIGN }) - host=MySqlAccountNamePart() { definer.setHost(host); } ] - { return definer; } + [ "=" ( token= | token= ) { + requireDdlSyntax("MERGE".equalsIgnoreCase(token.image) || "TEMPTABLE".equalsIgnoreCase(token.image) + || "UNDEFINED".equalsIgnoreCase(token.image), "Expected MERGE, TEMPTABLE, or UNDEFINED"); + options.setAlgorithm(MySqlViewOptions.Algorithm.valueOf(token.image.toUpperCase(Locale.ROOT))); + present = true; + } ] + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) { definer = new MySqlDefiner(); } + MySqlDefiner(definer) { options.setDefiner(definer); present = true; } ] + [ LOOKAHEAD({ isKeywordAhead("SQL") }) ContextualKeyword("SQL") token= { + requireDdlSyntax("DEFINER".equalsIgnoreCase(token.image) || "INVOKER".equalsIgnoreCase(token.image), + "Expected DEFINER or INVOKER"); + options.setSecurity(MySqlViewOptions.Security.valueOf(token.image.toUpperCase(Locale.ROOT))); + present = true; + } ] + { return present ? options : null; } } CreateTrigger.Timing MySqlTriggerTiming(): @@ -14578,7 +14613,7 @@ CreateSchema CreateSchema(): { [ LOOKAHEAD(2) { schema.setIfNotExists(true); } ] - [ + [ LOOKAHEAD(1) ( tk= | tk=) { schema.setSchemaName(tk.image); } ( @@ -15752,9 +15787,12 @@ CreateView CreateView(boolean isUsingOrReplace): List viewOptions = new ArrayList(); ViewOption viewOption = null; boolean withData; + MySqlViewOptions mySqlOptions; + CreateView.CheckOption checkOption; } { { createView.setOrReplace(isUsingOrReplace);} + mySqlOptions=MySqlViewPrefix() { createView.setMySqlOptions(mySqlOptions); } [ { createView.setForce(ForceOption.NO_FORCE); } | { createView.setForce(ForceOption.FORCE); } @@ -15797,14 +15835,7 @@ CreateView CreateView(boolean isUsingOrReplace): ( { createView.setWithReadOnly(true); } | - [ { createView.setCheckOption(CreateView.CheckOption.LOCAL); } - | LOOKAHEAD({ isKeywordAhead("CASCADED") }) tk= - { createView.setCheckOption(CreateView.CheckOption.CASCADED); } ] - { - if (createView.getCheckOption() == null) { - createView.setCheckOption(CreateView.CheckOption.DEFAULT); - } - } + checkOption=ViewCheckOption() { createView.setCheckOption(checkOption); } | withData=DataPopulation() { createView.setWithData(withData); } ) @@ -15812,6 +15843,17 @@ CreateView CreateView(boolean isUsingOrReplace): { createView.validateOptions(); return createView; } } +CreateView.CheckOption ViewCheckOption(): +{ + CreateView.CheckOption option = CreateView.CheckOption.DEFAULT; +} +{ + [ { option = CreateView.CheckOption.LOCAL; } + | ContextualKeyword("CASCADED") { option = CreateView.CheckOption.CASCADED; } ] + + { return option; } +} + ViewOption PostgreSqlViewOption(): { String name; @@ -16072,15 +16114,19 @@ void AlterExpressionConstraintTail(AlterExpression alterExp, Index index): AlterView AlterView(boolean useReplace): { AlterView alterView = new AlterView(); + MySqlViewOptions options; + CreateView.CheckOption checkOption; Table view = null; Select select = null; List columnNames = null; } { + options=MySqlViewPrefix() { alterView.setMySqlOptions(options); } view=Table() { alterView.setView(view); alterView.setUseReplace(useReplace); } [ columnNames = ColumnsNamesList() { alterView.setColumnNames(columnNames); } ] select=Select() + [ LOOKAHEAD(2) checkOption=ViewCheckOption() { alterView.setCheckOption(checkOption); } ] { alterView.setSelect(select); return alterView; @@ -17969,7 +18015,7 @@ Statement Alter(): | statement = AlterSession() | - statement = AlterView(false) + LOOKAHEAD(MySqlViewPrefix() ) statement = AlterView(false) | statement = AlterSystemStatement() | @@ -18887,6 +18933,8 @@ Statement Create(): | statement = CreateEvent() | + LOOKAHEAD(MySqlViewPrefix() ) statement = CreateView(isUsingOrReplace) + | LOOKAHEAD({ getToken(1).kind == K_TRIGGER || "DEFINER".equalsIgnoreCase(getToken(1).image) }) statement = CreateTrigger() diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java new file mode 100644 index 000000000..29eeb07b8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlViewOptionsTest.java @@ -0,0 +1,108 @@ +/*- + * #%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; + +import static org.junit.jupiter.api.Assertions.*; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.*; +import net.sf.jsqlparser.util.TablesNamesFinder; +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 MySqlViewOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ALGORITHM = MERGE", "ALGORITHM = TEMPTABLE", + "ALGORITHM = UNDEFINED", "DEFINER = CURRENT_USER", "DEFINER = CURRENT_USER()", + "DEFINER = 'user'@'localhost'", "DEFINER = `user`@`localhost`", + "SQL SECURITY INVOKER", "SQL SECURITY DEFINER", + "ALGORITHM = MERGE DEFINER = CURRENT_USER SQL SECURITY INVOKER"}) + void createAndAlterSharePrefix(String options) throws JSQLParserException { + for (String operation : new String[] {"CREATE", "CREATE OR REPLACE", "ALTER"}) { + String sql = operation + " " + options + " VIEW v AS SELECT id FROM t"; + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + assertRoundTrip(statement); + assertTrue(new TablesNamesFinder().getTables(statement).contains("t")); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.MYSQL)).size()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CHECK OPTION", "LOCAL CHECK OPTION", "CASCADED CHECK OPTION"}) + void checkOptionTailIsShared(String tail) throws JSQLParserException { + for (String operation : new String[] {"CREATE", "ALTER"}) { + String sql = + operation + " SQL SECURITY INVOKER VIEW v AS SELECT id FROM t WITH " + tail; + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + assertRoundTrip(statement); + } + } + + @Test + void optionsAndDefinerAreMutable() throws JSQLParserException { + CreateView view = (CreateView) parse( + "CREATE ALGORITHM=MERGE DEFINER=CURRENT_USER() VIEW v AS SELECT 1"); + MySqlViewOptions options = view.getMySqlOptions(); + assertEquals(MySqlViewOptions.Algorithm.MERGE, options.getAlgorithm()); + assertTrue(options.getDefiner().isCurrentUserParentheses()); + options.getDefiner().setUser(new StringValue("owner")); + options.getDefiner().setHost(new StringValue("localhost")); + options.setAlgorithm(MySqlViewOptions.Algorithm.TEMPTABLE); + options.setSecurity(MySqlViewOptions.Security.INVOKER); + assertEquals( + "CREATE ALGORITHM = TEMPTABLE DEFINER = 'owner'@'localhost' SQL SECURITY INVOKER VIEW v AS SELECT 1", + view.toString()); + assertRoundTrip(view); + view.setMySqlOptions(null); + assertEquals("CREATE VIEW v AS SELECT 1", view.toString()); + } + + @Test + void sharedAccountParserPreservesTriggerDispatch() throws JSQLParserException { + for (String account : new String[] {"CURRENT_USER", "CURRENT_USER()", "'u'@'localhost'"}) { + CreateTrigger trigger = (CreateTrigger) parse("CREATE DEFINER = " + account + + " TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @a = 1"); + assertEquals(account, trigger.getDefiner().toString()); + assertRoundTrip(trigger); + } + } + + @Test + void invalidPrefixValuesFail() { + for (String sql : new String[] {"CREATE ALGORITHM=INVALID VIEW v AS SELECT 1", + "ALTER SQL SECURITY PUBLIC VIEW v AS SELECT 1"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + if (statement instanceof net.sf.jsqlparser.statement.UnsupportedStatement) { + throw new JSQLParserException("Expected a structured statement"); + } + return statement; + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +}