Skip to content

Commit 0f5c1be

Browse files
committed
Add @@error — always returns 0 because TRY/CATCH isn't modeled (every SimulatedSqlException terminates the batch, so only successful statements ever complete). LastErrorExpression mirrors TranCount/RowCount and provides the natural home for live tracking on BatchContext when TRY/CATCH lands.
1 parent f576a59 commit 0f5c1be

5 files changed

Lines changed: 80 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ Variable references resolve at runtime via a captured `VariableSlot` — require
279279

280280
**`@@ROWCOUNT`**: tracks the most-recently-completed statement's row count via `SimulatedDbConnection.LastStatementRowCount`. SELECT row counts populate after the dispatch materializes rows up-front (so the next statement in the batch sees the final count); DML mutations write their affected count; `SET` / `DECLARE @v = init` write 1; bare `DECLARE @v` (no initializer) preserves the prior count; transaction / DDL statements reset to 0.
281281

282+
**`@@ERROR`**: error number of the most-recently-completed statement; `int`. **Always 0 in the simulator** because TRY/CATCH isn't modeled — every `SimulatedSqlException` propagates out of the dispatch loop and terminates the batch, so only successful statements ever complete. Straight-line scripts that read `@@ERROR` after a known-good statement get correct behavior; scripts that wrap a statement-terminating-only error in `TRY ... CATCH` and expect to observe the number won't until TRY/CATCH lands — at which point `LastErrorExpression` becomes the natural home for live tracking on `BatchContext`.
283+
282284
**Compound assignment** (`SET @v += expr` etc.) and **table variables** (`DECLARE @t TABLE (...)`) aren't modeled — rewrite as `SET @v = @v + expr` for the former; the latter is a separate bundle.
283285

284286
### Common table expressions
@@ -433,7 +435,7 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
433435
- **`ALTER TABLE #foo`**, **`OBJECT_ID('tempdb..#foo')`** — none modeled (none of those exist for regular tables either yet). The common `IF OBJECT_ID(...) IS NOT NULL DROP TABLE` cleanup pattern works via `DROP TABLE IF EXISTS #foo` instead.
434436
- **Three-part name resolution outside DROP TABLE**: `tempdb..#foo` in FROM / INSERT / UPDATE / DELETE / MERGE / SET IDENTITY_INSERT raises `InvalidObjectName` (Msg 208) on the qualifier; use bare `#foo`. DROP TABLE alone tolerates the qualifier (probe pattern).
435437
- T-SQL `GOTO` / labels — `IF` / `BEGIN…END` / `WHILE` / `BREAK` / `CONTINUE` / `RETURN` (bare) ship; unconditional jumps don't.
436-
- `TRY ... CATCH`, `THROW`, `RAISERROR`, `@@ERROR`, stored procs / UDFs. `BEGIN TRY` / `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch (peeked after `BEGIN`). Value-form `RETURN N` raises Msg 178 (reserved for the stored-proc / function scope, neither modeled yet).
438+
- `TRY ... CATCH`, `THROW`, `RAISERROR`, stored procs / UDFs. `BEGIN TRY` / `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch (peeked after `BEGIN`). Value-form `RETURN N` raises Msg 178 (reserved for the stored-proc / function scope, neither modeled yet). `@@ERROR` parses + returns 0 (see batch-state section); live tracking lands with TRY/CATCH.
437439
- **`PRINT` message capture** — the statement parses + evaluates the operand (so operand-side errors like Msg 245 surface), but the message is discarded. `DbConnection` has no `InfoMessage` event (that's a `SqlConnection` extension), so adding a public observability surface would mean a new event on `SimulatedDbConnection`. Defer until an application needs it.
438440
- `hierarchyid`, `geography`, `geometry`.
439441

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Tests for <c>@@ERROR</c>. The simulator always returns 0 because TRY/CATCH
7+
/// isn't modeled — any <c>SimulatedSqlException</c> propagates out of the
8+
/// dispatch loop and terminates the batch, so the only statements that ever
9+
/// complete are successful ones and the most-recently-completed statement's
10+
/// error number is always 0. Apps that read <c>@@ERROR</c> in straight-line
11+
/// scripts (no TRY/CATCH wrapping) get correct behavior; apps that wrap a
12+
/// statement-terminating-only error in TRY/CATCH and expect to observe the
13+
/// number won't work until TRY/CATCH lands.
14+
/// </summary>
15+
[TestClass]
16+
public sealed class AtAtErrorTests
17+
{
18+
[TestMethod]
19+
public void AtAtError_BareRead_IsZero()
20+
=> AreEqual(0, new Simulation().ExecuteScalar("select @@error"));
21+
22+
[TestMethod]
23+
public void AtAtError_AfterSuccessfulInsert_IsZero()
24+
=> AreEqual(0, new Simulation().ExecuteScalar("""
25+
create table t (id int);
26+
insert t values (1);
27+
select @@error
28+
"""));
29+
30+
[TestMethod]
31+
public void AtAtError_CaseInsensitive()
32+
=> AreEqual(0, new Simulation().ExecuteScalar("select @@ERROR"));
33+
34+
[TestMethod]
35+
public void AtAtError_UsableInPredicate()
36+
=> AreEqual("ok", new Simulation().ExecuteScalar("""
37+
create table t (id int);
38+
insert t values (1);
39+
if @@error = 0 select 'ok'
40+
"""));
41+
42+
[TestMethod]
43+
public void AtAtError_ReturnsInt()
44+
{
45+
using var reader = new Simulation().ExecuteReader("select @@error as e");
46+
IsTrue(reader.Read());
47+
AreEqual(typeof(int), reader.GetFieldType(0));
48+
AreEqual(0, reader.GetInt32(0));
49+
}
50+
51+
[TestMethod]
52+
public void AtAtError_InArithmetic_IsZero()
53+
=> AreEqual(7, new Simulation().ExecuteScalar("select @@error + 7"));
54+
}

SqlServerSimulator/Parser/AtAtKeyword.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ enum AtAtKeyword
44
{
55
_ = 0, // "Default" value for the enum, indicating not a keyword.
66
Dbts,
7+
Error,
78
Identity,
89
LangId,
910
Language,

SqlServerSimulator/Parser/Expression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public static Expression Parse(ParserContext context)
6464
AtPrefixedString atPrefixed => new VariableReference(atPrefixed, context),
6565
DoubleAtPrefixedString doubleAtPrefixedString => doubleAtPrefixedString.Parse() switch
6666
{
67+
AtAtKeyword.Error => new LastErrorExpression(),
6768
AtAtKeyword.Identity => new LastIdentityExpression(),
6869
AtAtKeyword.TranCount => new TranCountExpression(context),
6970
AtAtKeyword.RowCount => new RowCountExpression(),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator.Parser.Expressions;
4+
5+
/// <summary>
6+
/// Backs <c>@@ERROR</c>: error number of the most recently completed
7+
/// statement, <see cref="SqlType.Int32"/>. Always returns 0 in the simulator
8+
/// because TRY/CATCH isn't modeled — any <see cref="SimulatedSqlException"/>
9+
/// propagates out of the dispatch loop and terminates the batch, so the only
10+
/// statements that ever complete are successful ones. When TRY/CATCH lands,
11+
/// this expression becomes the natural home for live error-number tracking
12+
/// against state on <see cref="BatchContext"/>.
13+
/// </summary>
14+
internal sealed class LastErrorExpression : Expression
15+
{
16+
public override SqlValue Run(RuntimeContext runtime) => SqlValue.FromInt32(0);
17+
18+
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Int32;
19+
20+
internal override string DebugDisplay() => "@@ERROR";
21+
}

0 commit comments

Comments
 (0)