Skip to content

Commit f473ccf

Browse files
Support SQL Server boolean SET options with shared statement rendering (#2604)
Co-authored-by: manticore-projects <andreas@manticore-projects.com>
1 parent bf2c9ac commit f473ccf

9 files changed

Lines changed: 320 additions & 46 deletions

File tree

src/main/java/net/sf/jsqlparser/parser/feature/Feature.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ public enum Feature {
683683
* @see SetStatement
684684
*/
685685
set,
686+
/** SQL Server SET option [, option] ON | OFF. */
687+
sqlServerSetOptions,
686688
/**
687689
* @see ResetStatement
688690
*/

src/main/java/net/sf/jsqlparser/statement/SetStatement.java

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,84 @@
1111

1212
import net.sf.jsqlparser.expression.Expression;
1313
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
14-
import net.sf.jsqlparser.statement.select.PlainSelect;
1514

1615
import java.io.Serializable;
1716
import java.util.ArrayList;
1817
import java.util.Arrays;
1918
import java.util.Collection;
2019
import java.util.List;
20+
import java.util.Objects;
21+
import java.util.function.Consumer;
2122

2223
public final class SetStatement implements Statement {
2324

2425
private final List<NameExpr> values = new ArrayList<>();
2526
private String effectParameter;
27+
private OnOffOptions onOffOptions;
28+
29+
/** SQL Server options that share the SET option [, option] ON | OFF syntax. */
30+
public enum OnOffOption {
31+
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;
32+
33+
public static OnOffOption fromName(String name) {
34+
for (OnOffOption option : values()) {
35+
if (option.name().equalsIgnoreCase(name)) {
36+
return option;
37+
}
38+
}
39+
return null;
40+
}
41+
}
42+
43+
/** A group of options sharing one ON/OFF value; separate from assignment expressions. */
44+
public static final class OnOffOptions implements Serializable {
45+
private final List<OnOffOption> options;
46+
private boolean on;
47+
48+
public OnOffOptions(Collection<OnOffOption> options, boolean on) {
49+
this.options = new ArrayList<>(options);
50+
if (this.options.isEmpty() || this.options.contains(null)) {
51+
throw new IllegalArgumentException("At least one non-null SET option is required");
52+
}
53+
this.on = on;
54+
}
55+
56+
public List<OnOffOption> getOptions() {
57+
return options;
58+
}
59+
60+
public boolean isOn() {
61+
return on;
62+
}
63+
64+
public void setOn(boolean on) {
65+
this.on = on;
66+
}
67+
68+
private StringBuilder appendTo(StringBuilder builder) {
69+
if (options.isEmpty() || options.contains(null)) {
70+
throw new IllegalStateException("Invalid SQL Server SET option group");
71+
}
72+
for (int i = 0; i < options.size(); i++) {
73+
if (i > 0) {
74+
builder.append(", ");
75+
}
76+
builder.append(options.get(i));
77+
}
78+
return builder.append(on ? " ON" : " OFF");
79+
}
80+
}
81+
82+
public OnOffOptions getOnOffOptions() {
83+
return onOffOptions;
84+
}
85+
86+
/** Selects SQL Server option syntax, clearing any existing assignments and scope. */
87+
public void setOnOffOptions(OnOffOptions onOffOptions) {
88+
Objects.requireNonNull(onOffOptions, "onOffOptions");
89+
clear();
90+
this.onOffOptions = onOffOptions;
91+
}
2692

2793
public SetStatement() {
2894
// empty constructor
@@ -33,6 +99,7 @@ public SetStatement(Object name, ExpressionList<?> value) {
3399
}
34100

35101
public void add(Object name, ExpressionList<?> value, boolean useEqual) {
102+
onOffOptions = null;
36103
values.add(new NameExpr(name, value, useEqual));
37104
}
38105

@@ -103,35 +170,53 @@ public void setExpressions(int idx, ExpressionList<?> expressions) {
103170
values.get(idx).expressions = expressions;
104171
}
105172

106-
private String toString(NameExpr ne) {
107-
return ne.name + (ne.useEqual ? " = " : " ")
108-
+ PlainSelect.getStringList(ne.expressions, true, false);
173+
/** Shares statement punctuation with deparsers while allowing expression visitors. */
174+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
175+
builder.append("SET ");
176+
if (onOffOptions != null) {
177+
if (!values.isEmpty() || effectParameter != null) {
178+
throw new IllegalStateException(
179+
"SET options cannot be combined with assignments or scope");
180+
}
181+
return onOffOptions.appendTo(builder);
182+
}
183+
if (effectParameter != null) {
184+
builder.append(effectParameter).append(" ");
185+
}
186+
for (int i = 0; i < values.size(); i++) {
187+
if (i > 0) {
188+
builder.append(", ");
189+
}
190+
appendAssignment(builder, values.get(i), expressionRenderer);
191+
}
192+
return builder;
109193
}
110194

111-
@Override
112-
public String toString() {
113-
StringBuilder b = new StringBuilder("SET ");
114-
if (effectParameter != null) {
115-
b.append(effectParameter).append(" ");
116-
}
117-
boolean addComma = false;
118-
for (NameExpr ne : values) {
119-
if (addComma) {
120-
b.append(", ");
121-
} else {
122-
addComma = true;
195+
private static void appendAssignment(StringBuilder builder, NameExpr value,
196+
Consumer<Expression> expressionRenderer) {
197+
builder.append(value.name).append(value.useEqual ? " = " : " ");
198+
if (value.expressions != null) {
199+
for (int i = 0; i < value.expressions.size(); i++) {
200+
if (i > 0) {
201+
builder.append(", ");
202+
}
203+
expressionRenderer.accept((Expression) value.expressions.get(i));
123204
}
124-
b.append(toString(ne));
125205
}
206+
}
126207

127-
return b.toString();
208+
@Override
209+
public String toString() {
210+
StringBuilder builder = new StringBuilder();
211+
return appendTo(builder, builder::append).toString();
128212
}
129213

130214
public List<NameExpr> getKeyValuePairs() {
131215
return values;
132216
}
133217

134218
public void addKeyValuePairs(Collection<NameExpr> keyValuePairs) {
219+
onOffOptions = null;
135220
values.addAll(keyValuePairs);
136221
}
137222

@@ -140,6 +225,7 @@ public void addKeyValuePairs(NameExpr... keyValuePairs) {
140225
}
141226

142227
public void clear() {
228+
onOffOptions = null;
143229
values.clear();
144230
effectParameter = null;
145231
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1733,7 +1733,9 @@ public void visit(Execute execute) {
17331733

17341734
@Override
17351735
public <S> Void visit(SetStatement setStatement, S context) {
1736-
throwUnsupported(setStatement);
1736+
if (setStatement.getOnOffOptions() == null) {
1737+
throwUnsupported(setStatement);
1738+
}
17371739
return null;
17381740
}
17391741

src/main/java/net/sf/jsqlparser/util/deparser/SetStatementDeParser.java

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@
99
*/
1010
package net.sf.jsqlparser.util.deparser;
1111

12-
import net.sf.jsqlparser.expression.Expression;
1312
import net.sf.jsqlparser.expression.ExpressionVisitor;
1413
import net.sf.jsqlparser.statement.SetStatement;
1514

16-
import java.util.List;
1715

1816
public class SetStatementDeParser extends AbstractDeParser<SetStatement> {
1917

@@ -27,28 +25,7 @@ public SetStatementDeParser(ExpressionVisitor<StringBuilder> expressionVisitor,
2725

2826
@Override
2927
public void deParse(SetStatement set) {
30-
builder.append("SET ");
31-
if (set.getEffectParameter() != null) {
32-
builder.append(set.getEffectParameter()).append(" ");
33-
}
34-
for (int i = 0; i < set.getCount(); i++) {
35-
if (i > 0) {
36-
builder.append(", ");
37-
}
38-
builder.append(set.getName(i));
39-
if (set.isUseEqual(i)) {
40-
builder.append(" =");
41-
}
42-
builder.append(" ");
43-
List<Expression> expressions = set.getExpressions(i);
44-
for (int j = 0; j < expressions.size(); j++) {
45-
if (j > 0) {
46-
builder.append(", ");
47-
}
48-
expressions.get(j).accept(expressionVisitor, null);
49-
}
50-
}
51-
28+
set.appendTo(builder, expression -> expression.accept(expressionVisitor, null));
5229
}
5330

5431
public ExpressionVisitor<StringBuilder> getExpressionVisitor() {

src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public enum SqlServerVersion implements Version {
8484
Feature.executeExec, Feature.executeExecute,
8585

8686
// https://docs.microsoft.com/en-us/sql/t-sql/language-elements/set-local-variable-transact-sql?view=sql-server-ver15
87-
Feature.set,
87+
Feature.set, Feature.sqlServerSetOptions,
8888

8989
// https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-transact-sql?view=sql-server-ver15
9090
Feature.alterTable, // https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-sequence-transact-sql?view=sql-server-ver15

src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class SetStatementValidator extends AbstractValidator<SetStatement> {
2323
public void validate(SetStatement set) {
2424
for (ValidationCapability c : getCapabilities()) {
2525
validateFeature(c, Feature.set);
26+
if (set.getOnOffOptions() != null) {
27+
validateFeature(c, Feature.sqlServerSetOptions);
28+
}
2629
}
2730
for (int i = 0; i < set.getCount(); i++) {
2831
validateOptionalExpressions(set.getExpressions(i));

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4068,7 +4068,22 @@ SessionStatement SessionStatement():
40684068
}
40694069

40704070
SetStatement Set(): {
4071-
String namePart;
4071+
SetStatement set;
4072+
}
4073+
{
4074+
<K_SET>
4075+
(
4076+
LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect))
4077+
&& getToken(1).kind == S_IDENTIFIER
4078+
&& SetStatement.OnOffOption.fromName(getToken(1).image) != null
4079+
&& !"=".equals(getToken(2).image) && !".".equals(getToken(2).image) })
4080+
set=SqlServerSetOnOffOptions()
4081+
| set=SetAssignments()
4082+
)
4083+
{ return set; }
4084+
}
4085+
4086+
SetStatement SetAssignments(): {
40724087
Object name;
40734088
ExpressionList expList;
40744089
boolean useEqual = false;
@@ -4078,7 +4093,6 @@ SetStatement Set(): {
40784093
String effectParameter = null;
40794094
}
40804095
{
4081-
<K_SET>
40824096
[LOOKAHEAD(3) (tk = <K_LOCAL> | tk = <K_SESSION>) {effectParameter = tk.image; } ]
40834097
(
40844098
LOOKAHEAD(2)
@@ -4126,6 +4140,33 @@ SetStatement Set(): {
41264140
{ return set; }
41274141
}
41284142

4143+
SetStatement SqlServerSetOnOffOptions(): {
4144+
SetStatement set = new SetStatement();
4145+
List<SetStatement.OnOffOption> options = new ArrayList<SetStatement.OnOffOption>();
4146+
SetStatement.OnOffOption option;
4147+
Token name;
4148+
boolean on;
4149+
}
4150+
{
4151+
name=<S_IDENTIFIER>
4152+
{
4153+
option = accessEnum(SetStatement.OnOffOption.class, name.image);
4154+
options.add(option);
4155+
}
4156+
(
4157+
"," name=<S_IDENTIFIER>
4158+
{
4159+
option = accessEnum(SetStatement.OnOffOption.class, name.image);
4160+
options.add(option);
4161+
}
4162+
)*
4163+
( <K_ON> { on = true; } | <K_OFF> { on = false; } )
4164+
{
4165+
set.setOnOffOptions(new SetStatement.OnOffOptions(options, on));
4166+
return set;
4167+
}
4168+
}
4169+
41294170
ResetStatement Reset(): {
41304171
String name;
41314172
ResetStatement reset;

src/site/sphinx/usage.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,12 @@ operators such as ``js#>>'{a}'`` and ``js#>'{a}'`` work without surrounding
788788
spaces. Quote identifiers containing ``#``, for example ``"js#"``. Other
789789
dialects retain their existing identifier and hash-comment rules.
790790

791+
With ``Dialect.SQLSERVER``, ``SET NOCOUNT ON`` and grouped boolean options such as
792+
``SET QUOTED_IDENTIFIER, ANSI_NULLS OFF`` use ``SetStatement.getOnOffOptions()``.
793+
The ordered ``OnOffOption`` list and shared ``isOn()`` value are editable;
794+
``setOnOffOptions()`` replaces generic assignments and their scope. Both SQL
795+
renderers share statement punctuation while generic assignments retain expression
796+
visitor support. Parsing a SET directive records it without changing lexer settings.
791797
``Dialect.SQLSERVER`` enables ``INSERT BULK table (name type, ...) WITH (...)``.
792798
``InsertBulk`` exposes the target table, existing ``ColumnDefinition`` models,
793799
and ordered typed options, including ``ROWS_PER_BATCH`` and ``ORDER`` keys.

0 commit comments

Comments
 (0)