diff --git a/src/main/java/net/sf/jsqlparser/statement/SetStatement.java b/src/main/java/net/sf/jsqlparser/statement/SetStatement.java index 6edca1a1e..f7745a164 100644 --- a/src/main/java/net/sf/jsqlparser/statement/SetStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/SetStatement.java @@ -26,6 +26,21 @@ public final class SetStatement implements Statement { private String effectParameter; private OnOffOptions onOffOptions; + /** Assignment punctuation; NONE also supports SET options and PostgreSQL value lists. */ + public enum AssignmentOperator { + NONE(""), EQUALS("="), COLON_EQUALS(":="); + + private final String token; + + AssignmentOperator(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + } + /** SQL Server options that share the SET option [, option] ON | OFF syntax. */ public enum OnOffOption { QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, CURSOR_CLOSE_ON_COMMIT, ARITHABORT, ARITHIGNORE, FMTONLY, NOCOUNT, NOEXEC, NUMERIC_ROUNDABORT, PARSEONLY, ANSI_DEFAULTS, ANSI_NULL_DFLT_OFF, ANSI_NULL_DFLT_ON, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, FORCEPLAN, SHOWPLAN_ALL, SHOWPLAN_TEXT, SHOWPLAN_XML, IMPLICIT_TRANSACTIONS, REMOTE_PROC_TRANSACTIONS, XACT_ABORT; @@ -99,8 +114,12 @@ public SetStatement(Object name, ExpressionList value) { } public void add(Object name, ExpressionList value, boolean useEqual) { + add(name, value, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE); + } + + public void add(Object name, ExpressionList value, AssignmentOperator operator) { onOffOptions = null; - values.add(new NameExpr(name, value, useEqual)); + values.add(new NameExpr(name, value, operator)); } public void remove(int idx) { @@ -112,7 +131,7 @@ public int getCount() { } public boolean isUseEqual(int idx) { - return values.get(idx).useEqual; + return values.get(idx).isUseEqual(); } public boolean isUseEqual() { @@ -129,10 +148,27 @@ public SetStatement withUseEqual(int idx, boolean useEqual) { } public SetStatement setUseEqual(int idx, boolean useEqual) { - values.get(idx).useEqual = useEqual; + values.get(idx).setUseEqual(useEqual); return this; } + public AssignmentOperator getAssignmentOperator(int idx) { + return values.get(idx).operator; + } + + public AssignmentOperator getAssignmentOperator() { + return getAssignmentOperator(0); + } + + public SetStatement setAssignmentOperator(int idx, AssignmentOperator operator) { + values.get(idx).operator = Objects.requireNonNull(operator, "operator"); + return this; + } + + public SetStatement setAssignmentOperator(AssignmentOperator operator) { + return setAssignmentOperator(0, operator); + } + public SetStatement withUseEqual(boolean useEqual) { this.setUseEqual(useEqual); return this; @@ -194,7 +230,10 @@ public StringBuilder appendTo(StringBuilder builder, Consumer expres private static void appendAssignment(StringBuilder builder, NameExpr value, Consumer expressionRenderer) { - builder.append(value.name).append(value.useEqual ? " = " : " "); + builder.append(value.name).append(" "); + if (value.operator != AssignmentOperator.NONE) { + builder.append(value.operator.getToken()).append(" "); + } if (value.expressions != null) { for (int i = 0; i < value.expressions.size(); i++) { if (i > 0) { @@ -251,12 +290,16 @@ public SetStatement withEffectParameter(String effectParameter) { static class NameExpr implements Serializable { Object name; ExpressionList expressions; - boolean useEqual; + AssignmentOperator operator; public NameExpr(Object name, ExpressionList expressions, boolean useEqual) { + this(name, expressions, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE); + } + + public NameExpr(Object name, ExpressionList expressions, AssignmentOperator operator) { this.name = name; this.expressions = expressions; - this.useEqual = useEqual; + this.operator = Objects.requireNonNull(operator, "operator"); } public Object getName() { @@ -276,11 +319,11 @@ public void setExpressions(ExpressionList expressions) { } public boolean isUseEqual() { - return useEqual; + return operator == AssignmentOperator.EQUALS; } public void setUseEqual(boolean useEqual) { - this.useEqual = useEqual; + operator = useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE; } } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5e6d9c46e..7e54920a7 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -4481,62 +4481,60 @@ SetStatement Set(): { } SetStatement SetAssignments(): { - Object name; - ExpressionList expList; - boolean useEqual = false; - SetStatement set; - Expression exp = null; + SetStatement set = new SetStatement(); + Expression exp; Token tk = null; - String effectParameter = null; } { - [LOOKAHEAD(3) (tk = | tk = ) {effectParameter = tk.image; } ] + [LOOKAHEAD(3) (tk = | tk = ) { set.setEffectParameter(tk.image); } ] + SetAssignment(set) ( - LOOKAHEAD(2) - { name = "Time Zone"; useEqual=false; } - | - ( - name = UserVariable() ["=" { useEqual=true; } ] - ) - | - ( - name = IdentifierChain() - ["=" { useEqual=true; } ] - ) - ) - exp=Expression() - { - expList = new ExpressionList(); - expList.add(exp); - set = new SetStatement(name, expList) - .withUseEqual(useEqual) - .withEffectParameter(effectParameter); - } - - ( - { useEqual=false; } "," - (LOOKAHEAD(3) - ( - ( LOOKAHEAD(2) - { name = "Time Zone"; useEqual=false; } - | - (name = RelObjectName() ["=" { useEqual=true; } ]) - ) - exp=Expression() - { - expList = new ExpressionList(); - expList.add(exp); - set.add(name, expList, useEqual); - } - ) - | - exp=Expression() { expList.add(exp); } - ) + ( LOOKAHEAD(3) SetAssignment(set) + | exp=Expression() { set.getExpressions(set.getCount() - 1).add(exp); } ) )* { return set; } } +/** The first and subsequent assignments use identical names and operators. */ +void SetAssignment(SetStatement set): +{ + Object name; + Expression expression; + SetStatement.AssignmentOperator operator = SetStatement.AssignmentOperator.NONE; +} +{ + ( + LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET) + || (isKeywordAhead("CHARSET") && !"=".equals(getToken(2).image) + && !":=".equals(getToken(2).image)) }) SetCharacterSet(set) + | + ( LOOKAHEAD(2) { name = "Time Zone"; } + | name=UserVariable() + | name=IdentifierChain() ) + [ "=" { operator = SetStatement.AssignmentOperator.EQUALS; } + | ":=" { operator = SetStatement.AssignmentOperator.COLON_EQUALS; } ] + expression=Expression() + { set.add(name, new ExpressionList(expression), operator); } + ) +} + +/** Character set selection has a single name or DEFAULT, not an arbitrary expression. */ +void SetCharacterSet(SetStatement set): +{ + String name; + Token value; + Expression expression; +} +{ + ( { name = "CHARACTER SET"; } + | { name = "CHARSET"; } ) + ( value= { expression = new StringValue(value.image); } + | (value= | value= | value=) + { expression = new Column(value.image); } ) + { set.add(name, new ExpressionList(expression), false); } +} + SetStatement SqlServerSetOnOffOptions(): { SetStatement set = new SetStatement(); List options = new ArrayList(); diff --git a/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java new file mode 100644 index 000000000..1b44e79c1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/MySqlSetStatementTest.java @@ -0,0 +1,122 @@ +/*- + * #%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 net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.SetStatement.AssignmentOperator; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +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; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MySqlSetStatementTest { + private SetStatement parse(String sql) throws Exception { + return (SetStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"SET @a := 1", "SET @a=1, @b:=2", "SET @a:=1, @b=2, @c:=3", + "SET @@session.sql_mode := ''", "SET SESSION sql_mode = ''", + "SET @a = (@b := 2), @c := 3", "SET CHARACTER SET utf8mb4", + "SET CHARACTER SET 'utf8mb4'", "SET CHARACTER SET DEFAULT", "SET CHARSET utf8mb4", + "SET CHARSET DEFAULT", "SET NAMES utf8mb4 COLLATE utf8mb4_bin", + "SET CHARSET DEFAULT, @a=1", "SET @a=1, CHARACTER SET utf8mb4"}) + void preservesSetSyntax(String sql) throws Exception { + SetStatement statement = parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } + + @Test + void exposesEachAssignmentAndOperator() throws Exception { + SetStatement statement = parse("SET @a=1, @b:=2, @c=3"); + assertEquals(3, statement.getCount()); + assertEquals("@b", statement.getName(1).toString()); + assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(1)); + assertEquals(1, statement.getExpressions(1).size()); + assertInstanceOf(LongValue.class, statement.getExpressions(1).get(0)); + statement.setExpressions(1, new ExpressionList<>(new LongValue(9))); + statement.setAssignmentOperator(2, AssignmentOperator.COLON_EQUALS); + assertEquals("SET @a = 1, @b := 9, @c := 3", statement.toString()); + statement.setUseEqual(1, true); + assertEquals(AssignmentOperator.EQUALS, statement.getAssignmentOperator(1)); + assertTrue(statement.isUseEqual(1)); + statement.remove(0); + assertEquals("SET @b = 9, @c := 3", statement.toString()); + } + + @Test + void exposesCharsetSelectionAlongsideAssignments() throws Exception { + SetStatement statement = parse("SET @a=1, CHARACTER SET utf8mb4, @b:=2"); + assertEquals(3, statement.getCount()); + assertEquals("CHARACTER SET", statement.getName(1)); + assertEquals(AssignmentOperator.NONE, statement.getAssignmentOperator(1)); + assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(2)); + } + + @Test + void keepsNestedAssignmentInItsValue() throws Exception { + SetStatement statement = parse("SET @a = (@b := 2), @c := 3"); + assertEquals(2, statement.getCount()); + assertEquals("@a", statement.getName().toString()); + assertEquals(1, statement.getExpressions().size()); + assertEquals("(@b := 2)", statement.getExpressions().get(0).toString()); + } + + @Test + void retainsPostgresqlValueListsAndLegacyApi() throws Exception { + SetStatement statement = (SetStatement) CCJSqlParserUtil.parse( + "SET search_path = public, extensions", p -> p.withDialect(Dialect.POSTGRESQL)); + assertEquals(1, statement.getCount()); + assertEquals(2, statement.getExpressions().size()); + assertEquals("SET search_path = public, extensions", statement.toString()); + SetStatement legacy = new SetStatement("x", new ExpressionList<>(new LongValue(1))); + assertEquals(AssignmentOperator.EQUALS, legacy.getAssignmentOperator()); + legacy.setUseEqual(false); + assertEquals("SET x 1", legacy.toString()); + legacy.setAssignmentOperator(AssignmentOperator.COLON_EQUALS); + assertEquals("SET x := 1", legacy.toString()); + } + + @Test + void visitsAllAssignmentValues() throws Exception { + List values = new java.util.ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + values.add(value.getValue()); + return getBuilder().append(value.getValue() + 10); + } + }; + StringBuilder output = new StringBuilder(); + parse("SET @a:=1, @b=2").accept( + new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of(1L, 2L), values); + assertEquals("SET @a := 11, @b = 12", output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SET @a :=", "SET @a := 1,", "SET CHARACTER SET", + "SET CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", "SET CHARSET DEFAULT,"}) + void rejectsIncompleteAssignmentsAndInvalidCharsetClauses(String sql) { + assertThrows(Exception.class, () -> parse(sql)); + } +}