Skip to content

Commit c5df9d2

Browse files
committed
ALTER DATABASE database-scope option dispatcher — first bacpac prerequisite. Closed-accept-list dict in Simulation.Alter.cs (RecognizedDatabaseOptions) covers the 16 options SqlPackage emits from a bacpac's SqlDatabaseOptions element, dispatched across 5 value shapes (OnOff / EqualsOnOff / EnumIdent / IntegerWithUnit / QueryStore). QUERY_STORE has its own nested grammar (= ON [( … )] | = OFF | CLEAR [ALL]) with a closed 10-name sub-option accept-list; CLEANUP_POLICY / QUERY_CAPTURE_POLICY nested-paren blocks eaten via SkipBalancedParens. Top-level ALTER DATABASE name COLLATE <name> ships as a hard-error path — NotSupportedException on anything other than SQL_Latin1_General_CP1_CI_AS (silent accept would mean the bacpac loader silently mis-loads collation-sensitive data on non-default-collation models). Load-bearing options (COMPATIBILITY_LEVEL / ALLOW_SNAPSHOT_ISOLATION / READ_COMMITTED_SNAPSHOT) keep their dedicated handlers, dispatched via the existing ContextualKeyword check upstream of the new dict. Syntax shapes locked via probe matrix against SQL Server 2025 (2026-05-14): RECOVERY rejects =, ACCELERATED_DATABASE_RECOVERY / OPTIMIZED_LOCKING require =, TARGET_RECOVERY_TIME requires the unit suffix (SECONDS|MINUTES). 53 new tests in AlterDatabaseOptionsTests.cs cover every shape × option pairing, QUERY_STORE block forms (bare / single-sub-option / nested / multi / CLEAR / CLEAR ALL), COLLATE hard-error, the RCSI regression confirming the load-bearing wiring still threads, and the three probe-confirmed syntax-error paths. docs/claude/bacpac-prerequisites.md flips the database-options bullet from [ ] to [x] with implementation detail and renumbers the order-of-operations sequence; CLAUDE.md gains a sibling ALTER DATABASE paragraph next to the existing SET <option> one in the Not modeled section. IsFullTextEnabled deliberately not handled — it's emitted as EXEC sp_fulltext_database, deferred with the rest of full-text.
1 parent a1a4e3b commit c5df9d2

4 files changed

Lines changed: 426 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
163163
- **Key-range locks** — sole remaining phase 4+ deferral. Phases 0–3 + follow-ups ship comprehensive lock + MVCC: full Sch-S/Sch-M/IS/IX/SIX/S/U/X matrix, row-level + table-escalated modes, hint surface, Msg 1205/1222/1047/1065/1069/307/308 verbatim, SNAPSHOT + RCSI via `HeapTable.RowVersions` chains, ASI/RCSI flips, Msg 3952/3960, version-store GC at tx finalize, `sys.dm_tran_locks` + `sys.dm_os_waiting_tasks` + `sys.dm_tran_version_store` + `sys.dm_tran_version_store_space_usage` + `sys.dm_tran_active_snapshot_database_transactions` DMVs. Remaining phase-3 fidelity gap: multi-update-within-one-tx history collapse. See [`docs/claude/locking.md`](docs/claude/locking.md).
164164
- `BEGIN DISTRIBUTED TRANSACTION` and `BEGIN TRANSACTION ... WITH MARK` raise `NotSupportedException` at dispatch.
165165
- **`SET <option> …` parse-and-discard surface** — closed accept-list in `Simulation.Set.cs` covers `SET XACT_ABORT`, all 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 (`TEXTSIZE` / `DATEFIRST` / `ROWCOUNT` / `DATEFORMAT` / `LANGUAGE` / `DEADLOCK_PRIORITY` / `CONTEXT_INFO`), `SET STATISTICS {IO|TIME|XML|PROFILE} ON|OFF`. Unknown SET option with `ON`/`OFF`/value → Msg 195 verbatim (`'<name>' is not a recognized SET option.`); no-trailing-token form → generic Msg 102. `SET @v = expr` / `SET IDENTITY_INSERT` / `SET NOCOUNT` / `SET IMPLICIT_TRANSACTIONS` / `SET LOCK_TIMEOUT` / `SET TRANSACTION ISOLATION LEVEL {…}` (incl. SNAPSHOT — see locking.md for Msg 3952) carry semantic effect; everything else is grammar-only.
166+
- **`ALTER DATABASE name SET <option> …` database-scope surface** — closed accept-list in `Simulation.Alter.cs` (`RecognizedDatabaseOptions`) covers every option SqlPackage emits from a bacpac's `SqlDatabaseOptions` element: `ANSI_NULLS` / `ANSI_PADDING` / `ANSI_WARNINGS` / `ARITHABORT` / `CONCAT_NULL_YIELDS_NULL` / `NUMERIC_ROUNDABORT` / `QUOTED_IDENTIFIER` / `TORN_PAGE_DETECTION` / `TEMPORAL_HISTORY_RETENTION` (`OnOff` shape), `RECOVERY` / `PAGE_VERIFY` / `CURSOR_DEFAULT` (`EnumIdent`), `ACCELERATED_DATABASE_RECOVERY` / `OPTIMIZED_LOCKING` (`= ON|OFF` — `=` required per probe), `TARGET_RECOVERY_TIME` (`= N SECONDS|MINUTES` — unit required), `QUERY_STORE` (`= ON [( … )] | = OFF | CLEAR [ALL]` — sub-options block closed-accept-list-validated). `ALTER DATABASE name COLLATE <name>` hard-errors with `NotSupportedException` on non-default collation. Load-bearing options (`COMPATIBILITY_LEVEL`, `ALLOW_SNAPSHOT_ISOLATION`, `READ_COMMITTED_SNAPSHOT`) keep dedicated handlers and behavior wiring. See [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) for the bacpac-loader context.
166167
- `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.
167168
- Comma-separated FROM (legacy ANSI-89 join syntax).
168169
- `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: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Exercises the parse-and-discard surface for the database-scope
7+
/// <c>ALTER DATABASE name SET …</c> options the bacpac loader will emit on
8+
/// import. The semantic effect of these toggles isn't modeled (the simulator
9+
/// has no recovery model, no query store, no torn-page detector); the test
10+
/// goal is that each canonical T-SQL shape SqlPackage may emit parses
11+
/// without throwing. Shapes are probe-confirmed against SQL Server 2025
12+
/// (2026-05-14). Load-bearing options (COMPATIBILITY_LEVEL, ALLOW_SNAPSHOT_ISOLATION,
13+
/// READ_COMMITTED_SNAPSHOT) have separate behavior coverage in
14+
/// <see cref="CompatibilityLevelTests"/> and <see cref="SnapshotIsolationTests"/>.
15+
/// </summary>
16+
[TestClass]
17+
public class AlterDatabaseOptionsTests
18+
{
19+
[TestMethod]
20+
// ON/OFF (no `=`)
21+
[DataRow("ALTER DATABASE claude SET ANSI_NULLS ON")]
22+
[DataRow("ALTER DATABASE claude SET ANSI_NULLS OFF")]
23+
[DataRow("ALTER DATABASE claude SET ANSI_PADDING ON")]
24+
[DataRow("ALTER DATABASE claude SET ANSI_WARNINGS ON")]
25+
[DataRow("ALTER DATABASE claude SET ARITHABORT ON")]
26+
[DataRow("ALTER DATABASE claude SET CONCAT_NULL_YIELDS_NULL ON")]
27+
[DataRow("ALTER DATABASE claude SET NUMERIC_ROUNDABORT OFF")]
28+
[DataRow("ALTER DATABASE claude SET QUOTED_IDENTIFIER ON")]
29+
[DataRow("ALTER DATABASE claude SET TORN_PAGE_DETECTION OFF")]
30+
[DataRow("ALTER DATABASE claude SET TEMPORAL_HISTORY_RETENTION ON")]
31+
// Enum (bare identifier value)
32+
[DataRow("ALTER DATABASE claude SET RECOVERY FULL")]
33+
[DataRow("ALTER DATABASE claude SET RECOVERY BULK_LOGGED")]
34+
[DataRow("ALTER DATABASE claude SET RECOVERY SIMPLE")]
35+
[DataRow("ALTER DATABASE claude SET PAGE_VERIFY CHECKSUM")]
36+
[DataRow("ALTER DATABASE claude SET PAGE_VERIFY NONE")]
37+
[DataRow("ALTER DATABASE claude SET PAGE_VERIFY TORN_PAGE_DETECTION")]
38+
[DataRow("ALTER DATABASE claude SET CURSOR_DEFAULT GLOBAL")]
39+
[DataRow("ALTER DATABASE claude SET CURSOR_DEFAULT LOCAL")]
40+
// `= ON|OFF` (`=` required)
41+
[DataRow("ALTER DATABASE claude SET ACCELERATED_DATABASE_RECOVERY = ON")]
42+
[DataRow("ALTER DATABASE claude SET ACCELERATED_DATABASE_RECOVERY = OFF")]
43+
[DataRow("ALTER DATABASE claude SET OPTIMIZED_LOCKING = ON")]
44+
[DataRow("ALTER DATABASE claude SET OPTIMIZED_LOCKING = OFF")]
45+
// Integer with unit
46+
[DataRow("ALTER DATABASE claude SET TARGET_RECOVERY_TIME = 60 SECONDS")]
47+
[DataRow("ALTER DATABASE claude SET TARGET_RECOVERY_TIME = 1 MINUTES")]
48+
// CURRENT name
49+
[DataRow("ALTER DATABASE CURRENT SET ANSI_NULLS ON")]
50+
public void Option_ParsesAndDiscards(string sql)
51+
=> AreEqual(-1, new Simulation().ExecuteNonQuery(sql));
52+
53+
[TestMethod]
54+
// Bare ON/OFF — the simplest QUERY_STORE shape
55+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON")]
56+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = OFF")]
57+
// CLEAR / CLEAR ALL — no `=`
58+
[DataRow("ALTER DATABASE claude SET QUERY_STORE CLEAR")]
59+
[DataRow("ALTER DATABASE claude SET QUERY_STORE CLEAR ALL")]
60+
// Single sub-option
61+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE)")]
62+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (OPERATION_MODE = READ_ONLY)")]
63+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (DATA_FLUSH_INTERVAL_SECONDS = 900)")]
64+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (MAX_STORAGE_SIZE_MB = 1000)")]
65+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (INTERVAL_LENGTH_MINUTES = 30)")]
66+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (SIZE_BASED_CLEANUP_MODE = AUTO)")]
67+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (SIZE_BASED_CLEANUP_MODE = OFF)")]
68+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_MODE = ALL)")]
69+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_MODE = AUTO)")]
70+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_MODE = NONE)")]
71+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_MODE = CUSTOM)")]
72+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (MAX_PLANS_PER_QUERY = 200)")]
73+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (WAIT_STATS_CAPTURE_MODE = ON)")]
74+
// Nested sub-blocks
75+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30))")]
76+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_POLICY = (EXECUTION_COUNT = 10))")]
77+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (QUERY_CAPTURE_POLICY = (STALE_CAPTURE_POLICY_THRESHOLD = 24 HOURS, EXECUTION_COUNT = 30, TOTAL_COMPILE_CPU_TIME_MS = 1000, TOTAL_EXECUTION_CPU_TIME_MS = 100))")]
78+
// Multi-sub-option
79+
[DataRow("ALTER DATABASE claude SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE, INTERVAL_LENGTH_MINUTES = 30, MAX_STORAGE_SIZE_MB = 1000, QUERY_CAPTURE_MODE = AUTO)")]
80+
public void QueryStore_ParsesAndDiscards(string sql)
81+
=> AreEqual(-1, new Simulation().ExecuteNonQuery(sql));
82+
83+
[TestMethod]
84+
public void QueryStore_UnknownSubOption_RaisesSyntaxError()
85+
{
86+
// Probe-confirmed verbatim: SQL Server 2025 raises Msg 102 near the
87+
// first unknown sub-option name; the simulator's parse-and-discard
88+
// walks each sub-option through the closed accept-list and the first
89+
// unknown name surfaces as Msg 102.
90+
var ex = new Simulation().AssertSqlError(
91+
"ALTER DATABASE claude SET QUERY_STORE = ON (BOGUS_OPTION = 1)",
92+
102);
93+
Contains("BOGUS_OPTION", ex.Message);
94+
}
95+
96+
[TestMethod]
97+
public void Collate_Default_Accepts()
98+
=> AreEqual(-1, new Simulation().ExecuteNonQuery(
99+
"ALTER DATABASE claude COLLATE SQL_Latin1_General_CP1_CI_AS"));
100+
101+
[TestMethod]
102+
public void Collate_NonDefault_RaisesNotSupported()
103+
{
104+
var ex = Throws<NotSupportedException>(() =>
105+
new Simulation().ExecuteNonQuery("ALTER DATABASE claude COLLATE Japanese_CI_AS"));
106+
Contains("Japanese_CI_AS", ex.Message);
107+
Contains("SQL_Latin1_General_CP1_CI_AS", ex.Message);
108+
}
109+
110+
[TestMethod]
111+
public void Recovery_WithEqualsSign_RaisesSyntaxError()
112+
{
113+
// Probe-confirmed: SQL Server 2025 rejects `SET RECOVERY = FULL`
114+
// (the documented grammar is bare enum value, no `=`). Simulator
115+
// falls through to Msg 102 the same way.
116+
_ = new Simulation().AssertSqlError(
117+
"ALTER DATABASE claude SET RECOVERY = FULL", 102);
118+
}
119+
120+
[TestMethod]
121+
public void AcceleratedDatabaseRecovery_BareForm_RaisesSyntaxError()
122+
{
123+
// Probe-confirmed: SQL Server 2025 requires `= ON|OFF` for this option.
124+
_ = new Simulation().AssertSqlError(
125+
"ALTER DATABASE claude SET ACCELERATED_DATABASE_RECOVERY ON", 102);
126+
}
127+
128+
[TestMethod]
129+
public void TargetRecoveryTime_MissingUnit_RaisesSyntaxError()
130+
{
131+
// Probe-confirmed: the unit (SECONDS|MINUTES) is required.
132+
_ = new Simulation().AssertSqlError(
133+
"ALTER DATABASE claude SET TARGET_RECOVERY_TIME = 60", 102);
134+
}
135+
136+
[TestMethod]
137+
public void ReadCommittedSnapshot_StillWiredThrough()
138+
{
139+
// Regression: the dispatcher refactor must preserve the load-bearing
140+
// semantic effect for the three historically-shipped options.
141+
// RCSI is the easiest to observe externally.
142+
var sim = new Simulation();
143+
_ = sim.ExecuteNonQuery("ALTER DATABASE claude SET READ_COMMITTED_SNAPSHOT ON");
144+
// Indirect verification: SI iso under RCSI doesn't raise Msg 3952 on a
145+
// statement-level snapshot read.
146+
using var conn = sim.CreateOpenConnection();
147+
using var cmd = conn.CreateCommand("SELECT 1");
148+
AreEqual(1, cmd.ExecuteScalar());
149+
}
150+
}

0 commit comments

Comments
 (0)