Skip to content

Commit a035c48

Browse files
committed
TRY/CATCH + ERROR_*() + live @@error + THROW: catches SimulatedSqlException at the per-statement dispatch boundary when inside a TRY frame. Splits DispatchOneStatement into a wrapper + Core iterator (yield can't coexist with try/catch in C# iterators) so the wrapper can materialize Core's outcomes and intercept thrown errors into BatchContext.InFlightError + ErrorSignaled flag (added to IsSkipping). After a catch, the cursor scans forward to the next statement boundary so the outer dispatch loop can resume cleanly — without this scan it'd re-dispatch the same partially-parsed statement and infinite-loop. ERROR_NUMBER / ERROR_MESSAGE / ERROR_SEVERITY / ERROR_STATE / ERROR_LINE / ERROR_PROCEDURE zero-arg scalars read InFlightError and return typed NULL outside CATCH. @@error live-tracks Connection.LastErrorNumber (set by the wrap on catch, reset to 0 on successful statement completion); was hardcoded 0. THROW ships both forms — bare re-raise from CATCH (Msg 10704 outside) and value-form (number, message, state) with fixed severity 16. Nested TRY/CATCH saves+restores InFlightError around inner CATCH so re-throw paths surface correctly to the outer CATCH. Fidelity gaps: parse-time name-resolution errors ARE caught (no compile/runtime split — same root cause as Q15 un-taken-IF gap); divide-by-zero raises raw DivideByZeroException not SimulatedSqlException so TRY/CATCH can't catch it (pre-existing); RAISERROR and XACT_STATE/XACT_ABORT deferred.
1 parent 3ec7be4 commit a035c48

15 files changed

Lines changed: 1131 additions & 25 deletions

File tree

CLAUDE.md

Lines changed: 33 additions & 4 deletions
Large diffs are not rendered by default.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator.Parser;
4+
5+
/// <summary>
6+
/// Pins the <see cref="Keyword"/> enum against the authoritative T-SQL
7+
/// reserved keywords list at
8+
/// <c>https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql</c>.
9+
/// Any change to <see cref="Keyword"/> must be verified against that page —
10+
/// adding a contextual keyword (e.g. <c>THROW</c>, <c>TRY</c>, <c>CATCH</c>)
11+
/// to this enum is a bug because the tokenizer relies on the enum to decide
12+
/// what surfaces as <c>ReservedKeyword</c> vs. <c>UnquotedString</c>, and a
13+
/// non-reserved word being treated as reserved breaks valid SQL (e.g.
14+
/// blocking <c>select 1 as throw</c>).
15+
/// </summary>
16+
/// <remarks>
17+
/// The test cross-checks both directions: every entry in <see cref="Keyword"/>
18+
/// must appear in the canonical list, and every canonical entry must appear
19+
/// in <see cref="Keyword"/>. The single exception is <c>WITHIN GROUP</c>,
20+
/// which appears in the canonical list as a two-word entry but is omitted
21+
/// from the enum — <see cref="Keyword"/>'s trailing comment notes that
22+
/// <c>WITHIN</c> alone isn't enforced as a reserved word in real SQL Server
23+
/// and <c>GROUP</c> is already covered, so a multi-word enum entry would be
24+
/// functionally irrelevant. The omission is hardcoded into
25+
/// <see cref="DocumentedOmissions"/> rather than parameterized — the
26+
/// "reserved keyword list is frozen" rule has no anticipated exceptions
27+
/// beyond this one structural quirk.
28+
/// </remarks>
29+
[TestClass]
30+
public sealed class ReservedKeywordsTests
31+
{
32+
/// <summary>
33+
/// The canonical reserved-keyword set, transcribed verbatim from
34+
/// <c>https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql</c>.
35+
/// Updates here must be verified against that page; do not edit to
36+
/// match the enum without re-checking the source.
37+
/// </summary>
38+
private static readonly HashSet<string> CanonicalReservedKeywords =
39+
[
40+
"ADD", "ALL", "ALTER", "AND", "ANY", "AS", "ASC", "AUTHORIZATION",
41+
"BACKUP", "BEGIN", "BETWEEN", "BREAK", "BROWSE", "BULK", "BY",
42+
"CASCADE", "CASE", "CHECK", "CHECKPOINT", "CLOSE", "CLUSTERED", "COALESCE",
43+
"COLLATE", "COLUMN", "COMMIT", "COMPUTE", "CONSTRAINT", "CONTAINS",
44+
"CONTAINSTABLE", "CONTINUE", "CONVERT", "CREATE", "CROSS", "CURRENT",
45+
"CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "CURSOR",
46+
"DATABASE", "DBCC", "DEALLOCATE", "DECLARE", "DEFAULT", "DELETE", "DENY",
47+
"DESC", "DISK", "DISTINCT", "DISTRIBUTED", "DOUBLE", "DROP", "DUMP",
48+
"ELSE", "END", "ERRLVL", "ESCAPE", "EXCEPT", "EXEC", "EXECUTE", "EXISTS",
49+
"EXIT", "EXTERNAL",
50+
"FETCH", "FILE", "FILLFACTOR", "FOR", "FOREIGN", "FREETEXT", "FREETEXTTABLE",
51+
"FROM", "FULL", "FUNCTION",
52+
"GOTO", "GRANT", "GROUP",
53+
"HAVING", "HOLDLOCK",
54+
"IDENTITY", "IDENTITY_INSERT", "IDENTITYCOL", "IF", "IN", "INDEX", "INNER",
55+
"INSERT", "INTERSECT", "INTO", "IS",
56+
"JOIN",
57+
"KEY", "KILL",
58+
"LEFT", "LIKE", "LINENO", "LOAD",
59+
"MERGE",
60+
"NATIONAL", "NOCHECK", "NONCLUSTERED", "NOT", "NULL", "NULLIF",
61+
"OF", "OFF", "OFFSETS", "ON", "OPEN", "OPENDATASOURCE", "OPENQUERY",
62+
"OPENROWSET", "OPENXML", "OPTION", "OR", "ORDER", "OUTER", "OVER",
63+
"PERCENT", "PIVOT", "PLAN", "PRECISION", "PRIMARY", "PRINT", "PROC",
64+
"PROCEDURE", "PUBLIC",
65+
"RAISERROR", "READ", "READTEXT", "RECONFIGURE", "REFERENCES", "REPLICATION",
66+
"RESTORE", "RESTRICT", "RETURN", "REVERT", "REVOKE", "RIGHT", "ROLLBACK",
67+
"ROWCOUNT", "ROWGUIDCOL", "RULE",
68+
"SAVE", "SCHEMA", "SECURITYAUDIT", "SELECT", "SEMANTICKEYPHRASETABLE",
69+
"SEMANTICSIMILARITYDETAILSTABLE", "SEMANTICSIMILARITYTABLE", "SESSION_USER",
70+
"SET", "SETUSER", "SHUTDOWN", "SOME", "STATISTICS", "SYSTEM_USER",
71+
"TABLE", "TABLESAMPLE", "TEXTSIZE", "THEN", "TO", "TOP", "TRAN",
72+
"TRANSACTION", "TRIGGER", "TRUNCATE", "TRY_CONVERT", "TSEQUAL",
73+
"UNION", "UNIQUE", "UNPIVOT", "UPDATE", "UPDATETEXT", "USE", "USER",
74+
"VALUES", "VARYING", "VIEW",
75+
"WAITFOR", "WHEN", "WHERE", "WHILE", "WITH", "WITHIN GROUP", "WRITETEXT",
76+
];
77+
78+
/// <summary>
79+
/// The only canonical entry the enum intentionally omits — a multi-word
80+
/// reserved keyword whose component words don't independently behave as
81+
/// reserved in real SQL Server (see class doc).
82+
/// </summary>
83+
private static readonly HashSet<string> DocumentedOmissions = ["WITHIN GROUP"];
84+
85+
[TestMethod]
86+
public void Keyword_Enum_MatchesCanonicalReservedList()
87+
{
88+
var enumNames = Enum.GetNames<Keyword>()
89+
.Where(n => n != "_")
90+
.Select(n => n.ToUpperInvariant())
91+
.ToHashSet();
92+
93+
var unexpectedExtras = enumNames.Except(CanonicalReservedKeywords).Order().ToList();
94+
var unexpectedOmissions = CanonicalReservedKeywords.Except(enumNames).Except(DocumentedOmissions).Order().ToList();
95+
96+
IsEmpty(
97+
unexpectedExtras,
98+
$"Keyword enum has entries not in the canonical reserved list at {SourceUrl}: {string.Join(", ", unexpectedExtras)}. The reserved-keyword list is frozen — remove the entry from the enum (and route any contextual-keyword usage through UnquotedString, matching the TRY / CATCH / THROW pattern).");
99+
100+
IsEmpty(
101+
unexpectedOmissions,
102+
$"Keyword enum is missing canonical reserved keywords from {SourceUrl}: {string.Join(", ", unexpectedOmissions)}. Add them to the enum.");
103+
}
104+
105+
private const string SourceUrl = "https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql";
106+
}

SqlServerSimulator.Tests/IfBlockTests.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,6 @@ public void IfBody_NestedBlock_DispatchesInnerStatement()
171171
=> AreEqual("inside-if", new Simulation().ExecuteScalar(
172172
"begin if 1=1 select 'inside-if' end"));
173173

174-
[TestMethod]
175-
public void BeginTry_NotSupported()
176-
{
177-
var ex = Throws<NotSupportedException>(() => new Simulation().ExecuteNonQuery(
178-
"if 1=1 begin try select 1 end try begin catch select 2 end catch"));
179-
Contains("TRY", ex.Message, StringComparison.Ordinal);
180-
}
181-
182174
[TestMethod]
183175
public void BeginAtomic_NotSupported()
184176
=> Throws<NotSupportedException>(() => new Simulation().ExecuteNonQuery(

0 commit comments

Comments
 (0)