Skip to content

Commit c95032d

Browse files
committed
SET options (parse-and-discard for session / connection / planner state): expanded Simulation.Set.cs from a 2-option switch (NOCOUNT, IMPLICIT_TRANSACTIONS) into a closed accept-list covering the full standard SET surface. ANSI / session-state OnOff toggles: ANSI_NULLS, QUOTED_IDENTIFIER, ANSI_WARNINGS, ANSI_PADDING, CONCAT_NULL_YIELDS_NULL, ARITHABORT, ARITHIGNORE, NUMERIC_ROUNDABORT, XACT_ABORT, FMTONLY, NOEXEC, FORCEPLAN, PARSEONLY, CURSOR_CLOSE_ON_COMMIT, ANSI_DEFAULTS, REMOTE_PROC_TRANSACTIONS, NO_BROWSETABLE, NOCOUNT, IMPLICIT_TRANSACTIONS, SHOWPLAN_ALL, SHOWPLAN_TEXT, SHOWPLAN_XML, DISABLE_DEF_CNST_CHK. Value-taking options: LOCK_TIMEOUT / TEXTSIZE / DATEFIRST / ROWCOUNT / QUERY_GOVERNOR_COST_LIMIT (integer); DATEFORMAT / LANGUAGE (identifier or string-literal); DEADLOCK_PRIORITY (integer-or-identifier); CONTEXT_INFO (binary literal). Multi-word sub-forms: SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SNAPSHOT|SERIALIZABLE} and SET STATISTICS {IO|TIME|XML|PROFILE} ON|OFF — both flagged "not parsed" in CLAUDE.md previously, now ship as parse-and-discard since the underlying state (locking / IO accounting) isn't modeled anyway. ReservedKeyword-named options (ROWCOUNT / TEXTSIZE / STATISTICS / TRANSACTION) dispatch separately because they tokenize as ReservedKeyword rather than UnquotedString. **Multi-option comma form** (SET ANSI_NULLS, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, ANSI_WARNINGS, ANSI_PADDING ON) — the canonical EF Core SqlServer-provider session-bootstrap shape — restricted to OnOff-shaped options; unknown name inside the chain raises Msg 195 immediately. New UnrecognizedSetOption(name) factory mirrors probe wording verbatim: 'BANANA' is not a recognized SET option. (Msg 195, severity 15 state 1). Probe-confirmed split for unknown-option fall-throughs: SET BANANA ON raises Msg 195 (recognizable as SET-option-shape); SET BANANA with no trailing tokens raises Msg 102 generic syntax error (matching the real-server probe exactly — no dedicated rejection code when there's nothing to disambiguate). The simulator's Msg 102 path uses a checkpoint/restore so the rejection wording reports Incorrect syntax near 'BANANA' (the offending name) rather than near '' (post-EOF cursor). 50 new SetOptionTests cover every accepted entry plus both rejection paths plus the multi-option-with-unknown-in-the-middle chain. CLAUDE.md "Not modeled" rewritten to reflect the new shipping surface: SET TRANSACTION ISOLATION LEVEL and XACT_ABORT removed from the "not parsed" list; the closed accept-list and Msg 195 / Msg 102 split documented inline. BEGIN DISTRIBUTED TRANSACTION / BEGIN TRANSACTION ... WITH MARK continue to raise NotSupportedException at dispatch — the SET-side parse-and-discard doesn't apply to BEGIN-side multi-word transaction shapes.
1 parent 3efc41e commit c95032d

4 files changed

Lines changed: 337 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
158158

159159
## Not modeled
160160

161-
- Locks / MVCC / isolation levels (single-Simulation, single-thread-at-a-time). `BEGIN DISTRIBUTED TRANSACTION`, `BEGIN TRANSACTION ... WITH MARK`, `XACT_ABORT`, `SET TRANSACTION ISOLATION LEVEL` not parsed.
161+
- Locks / MVCC / isolation levels (single-Simulation, single-thread-at-a-time). `BEGIN DISTRIBUTED TRANSACTION` and `BEGIN TRANSACTION ... WITH MARK` raise `NotSupportedException` at dispatch. **Most `SET <option> …` statements are parsed-and-discarded** via a closed accept-list in `Simulation.Set.cs` — including `SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SNAPSHOT|SERIALIZABLE}`, `SET XACT_ABORT ON|OFF`, the ANSI / session-state toggles (ANSI_NULLS / QUOTED_IDENTIFIER / ANSI_WARNINGS / ANSI_PADDING / CONCAT_NULL_YIELDS_NULL / ARITHABORT / NUMERIC_ROUNDABORT / …) including the multi-option comma form EF Core emits, value-taking options (LOCK_TIMEOUT / TEXTSIZE / DATEFIRST / ROWCOUNT / DATEFORMAT / LANGUAGE / DEADLOCK_PRIORITY / CONTEXT_INFO), and the `SET STATISTICS {IO|TIME|XML|PROFILE} ON|OFF` sub-form. Unknown SET option followed by ON/OFF/value → Msg 195 verbatim (`'<name>' is not a recognized SET option.`); no-trailing-token form → generic Msg 102 (probe-confirmed split). Only `SET @v = expr`, `SET IDENTITY_INSERT`, `SET NOCOUNT ON|OFF`, and `SET IMPLICIT_TRANSACTIONS ON|OFF` have semantic effect; the closed accept-list now subsumes the last two for parse purposes, and the first two retain their dedicated semantic handlers.
162162
- `RIGHT JOIN` / `FULL OUTER JOIN` with a derived-table or lateral right side — base tables / views / system tables on the right ship (see JOINs / APPLY); a derived-table right (always-deferred in this simulator) raises `NotSupportedException` since real SQL Server's Msg 4104 rejection of correlated subqueries on the right of RIGHT/FULL can't be statically distinguished from non-correlated ones.
163163
- Comma-separated FROM (legacy ANSI-89 join syntax).
164164
- `RANGE BETWEEN <N> PRECEDING` / `<N> FOLLOWING` — real SQL Server gates the numeric-offset RANGE form behind a separately-licensed feature surface and the simulator matches that rejection (Msg 4194). `ROWS` numeric-offset frames ship; both modes support the canonical `UNBOUNDED` / `CURRENT ROW` bounds and the single-bound shorthand (`ROWS UNBOUNDED PRECEDING``ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`). The default frame when ORDER BY is present in OVER is `RANGE UNBOUNDED PRECEDING TO CURRENT ROW` (running-total semantic with peer-tie grouping); without ORDER BY, default frame is whole partition. LAST_VALUE ships with the same semantics as real SQL Server (its default frame returns the current row's value or the peer-tie last under RANGE — the intuitive "partition last" form needs explicit `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`).
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Closed-list <c>SET</c> session / connection / planner options —
7+
/// parse-and-discard for grammar compatibility (no underlying state
8+
/// modeling). Probe-confirmed verbatim against SQL Server 2025
9+
/// (2026-05-14): unknown option name followed by ON/OFF/value raises
10+
/// Msg 195; unknown name with nothing parseable after falls through
11+
/// to the generic Msg 102. <c>SET @v = expr</c> / <c>SET IDENTITY_INSERT</c>
12+
/// have semantic effect and are tested elsewhere.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class SetOptionTests
16+
{
17+
private static int RunBatch(string commandText) => new Simulation().ExecuteNonQuery(commandText);
18+
19+
[TestMethod]
20+
// Bool toggles — every entry in the OnOff family of the closed list.
21+
[DataRow("SET ANSI_NULLS ON")]
22+
[DataRow("SET ANSI_NULLS OFF")]
23+
[DataRow("SET QUOTED_IDENTIFIER ON")]
24+
[DataRow("SET ANSI_WARNINGS ON")]
25+
[DataRow("SET ANSI_PADDING ON")]
26+
[DataRow("SET CONCAT_NULL_YIELDS_NULL ON")]
27+
[DataRow("SET NUMERIC_ROUNDABORT OFF")]
28+
[DataRow("SET ARITHABORT ON")]
29+
[DataRow("SET ARITHIGNORE OFF")]
30+
[DataRow("SET XACT_ABORT ON")]
31+
[DataRow("SET FMTONLY OFF")]
32+
[DataRow("SET NOEXEC OFF")]
33+
[DataRow("SET FORCEPLAN OFF")]
34+
[DataRow("SET PARSEONLY OFF")]
35+
[DataRow("SET CURSOR_CLOSE_ON_COMMIT OFF")]
36+
[DataRow("SET ANSI_DEFAULTS ON")]
37+
[DataRow("SET REMOTE_PROC_TRANSACTIONS ON")]
38+
[DataRow("SET NO_BROWSETABLE OFF")]
39+
[DataRow("SET SHOWPLAN_TEXT OFF")]
40+
[DataRow("SET SHOWPLAN_ALL OFF")]
41+
[DataRow("SET SHOWPLAN_XML OFF")]
42+
[DataRow("SET DISABLE_DEF_CNST_CHK ON")]
43+
[DataRow("SET NOCOUNT ON")]
44+
[DataRow("SET IMPLICIT_TRANSACTIONS OFF")]
45+
// Multi-option comma form — OnOff-restricted. The five-toggle row is the
46+
// canonical EF Core SqlServer-provider session-bootstrap shape and was the
47+
// original motivating case for the closed-list parser.
48+
[DataRow("SET ANSI_NULLS, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, ANSI_WARNINGS, ANSI_PADDING ON")]
49+
[DataRow("SET ANSI_NULLS, QUOTED_IDENTIFIER OFF")]
50+
// Integer-value options (ROWCOUNT / TEXTSIZE tokenize as ReservedKeyword and
51+
// dispatch through the separate switch arm; the others come through
52+
// UnquotedString → closed-list lookup).
53+
[DataRow("SET LOCK_TIMEOUT 5000")]
54+
[DataRow("SET TEXTSIZE 4096")]
55+
[DataRow("SET DATEFIRST 7")]
56+
[DataRow("SET ROWCOUNT 100")]
57+
[DataRow("SET QUERY_GOVERNOR_COST_LIMIT 1000")]
58+
// Identifier-value options. LANGUAGE accepts both bare identifier and
59+
// quoted-string literal forms.
60+
[DataRow("SET DATEFORMAT mdy")]
61+
[DataRow("SET LANGUAGE us_english")]
62+
[DataRow("SET LANGUAGE N'us_english'")]
63+
// IntegerOrIdent options.
64+
[DataRow("SET DEADLOCK_PRIORITY LOW")]
65+
[DataRow("SET DEADLOCK_PRIORITY 5")]
66+
// Binary value.
67+
[DataRow("SET CONTEXT_INFO 0x12345678")]
68+
// SET TRANSACTION ISOLATION LEVEL — all five levels.
69+
[DataRow("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")]
70+
[DataRow("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")]
71+
[DataRow("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")]
72+
[DataRow("SET TRANSACTION ISOLATION LEVEL SNAPSHOT")]
73+
[DataRow("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")]
74+
// SET STATISTICS sub-form.
75+
[DataRow("SET STATISTICS IO ON")]
76+
[DataRow("SET STATISTICS TIME OFF")]
77+
[DataRow("SET STATISTICS XML OFF")]
78+
[DataRow("SET STATISTICS PROFILE OFF")]
79+
public void Accepted_NoOp(string sql) => AreEqual(-1, RunBatch(sql));
80+
81+
[TestMethod]
82+
[DataRow("SET BANANA ON", "BANANA")]
83+
[DataRow("SET ANSI_NULLS, BANANA, QUOTED_IDENTIFIER ON", "BANANA")]
84+
public void UnknownOption_RaisesMsg195(string sql, string unrecognizedName)
85+
=> new Simulation().AssertSqlError(sql, 195, $"'{unrecognizedName}' is not a recognized SET option.");
86+
87+
[TestMethod]
88+
public void UnknownOption_NoTrailingTokens_RaisesMsg102()
89+
{
90+
// Probe-confirmed: SQL Server returns the generic Msg 102 here
91+
// (no dedicated Msg 195 because there's nothing to disambiguate
92+
// SET option from arbitrary token sequence).
93+
var ex = new Simulation().AssertSqlError("SET BANANA", 102);
94+
Contains("BANANA", ex.Message);
95+
}
96+
97+
[TestMethod]
98+
public void Composed_WithSubsequentStatement_AcceptsBoth()
99+
{
100+
// The session-bootstrap-then-real-query pattern: SET options first,
101+
// then a SELECT. Statement-boundary handling must let the SET parser
102+
// hand off cleanly.
103+
AreEqual(1, new Simulation().ExecuteScalar("SET ANSI_NULLS ON; SELECT 1"));
104+
}
105+
}

SqlServerSimulator/Errors/SimulatedSqlException.SyntaxErrors.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ internal static SimulatedSqlException UnclosedStringLiteral() =>
3232

3333
internal static SimulatedSqlException SyntaxErrorNear(char c) => new($"Incorrect syntax near '{c}'.", 102, 15, 1);
3434

35+
/// <summary>
36+
/// Mimics SQL Server error 195: a <c>SET</c> statement names an option
37+
/// that isn't in the recognized set — and the parser saw enough of the
38+
/// rest of the shape (ON/OFF or a value token) to recognize it was meant
39+
/// as a SET option. Wording is probe-confirmed verbatim against SQL Server
40+
/// 2025 (2026-05-14): the offending name is preserved verbatim (uppercase
41+
/// in the probe) inside single quotes. The narrower failure mode where the
42+
/// name isn't followed by anything parseable falls through to the generic
43+
/// Msg 102 path instead.
44+
/// </summary>
45+
internal static SimulatedSqlException UnrecognizedSetOption(string name) =>
46+
new($"'{name}' is not a recognized SET option.", 195, 15, 1);
47+
3548
/// <summary>
3649
/// Mimics SQL Server error 111: the given <paramref name="statementKind"/>
3750
/// (e.g. <c>"CREATE/ALTER PROCEDURE"</c>, <c>"CREATE VIEW"</c>) must be

0 commit comments

Comments
 (0)