Skip to content

Commit c03193c

Browse files
committed
Per-batch scalar variables: DECLARE @v TYPE [= expr] [, @w ...] (multi-decl + inline init + optional AS), SET @v = expr, and SELECT @v = expr [, @w = expr2 ...] [FROM ...] (multi-assign, last-row-wins post-ORDER-BY, empty-result-keeps-prior, mixed-with-projection → Msg 141, yields SimulatedNonQuery so no result-set envelope). SqlClient parameters seed the same per-batch Variables dictionary as if pre-DECLAREd, sharing a namespace (collision → Msg 134); InputOutput/Output parameters write their final slot value back to DbParameter.Value at end of batch. VariableReference captures a live VariableSlot at parse time and reads slot.Value at runtime so SET/SELECT-assign mutations are observable; assignment coercion routes through Cast.ApplyCoercion so slot type fidelity is honored (SET @v(varchar(3)) = 'hello' truncates to 'hel'; SET @v(int) = 'abc' → Msg 245). @@rowcount tracks the last-completed statement via Simulation.LastStatementRowCount — SELECT materializes rows up-front so the next statement sees the final count, DML writes RecordsAffected, SET/DECLARE-with-init write 1, bare DECLARE preserves prior, transaction/DDL reset to 0. Compound assignment (+= etc.), table variables, control flow, TRY/CATCH, and implicit statement separation deferred.
1 parent 584c43a commit c03193c

18 files changed

Lines changed: 736 additions & 36 deletions

CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,25 @@ Three entry points share one per-connection undo log: implicit (statement-level
250250
### `rowversion` (legacy synonym `timestamp`)
251251
8-byte big-endian database-scoped monotonic counter; advances on every INSERT into a rowversion-bearing table and every UPDATE affecting one. Storage type name surfaces as `timestamp` in `information_schema` regardless of declaration. Explicit insert → Msg 273; explicit update → Msg 272; second column on a table → Msg 2738. Outbound CAST: `varbinary(N)`/`binary(N)` copy 8 bytes; `bigint` reads big-endian. `Promote(RowVersion, Varbinary) → Varbinary` so EF's `WHERE [rv] = @originalRv` parameter works directly. EF `[Timestamp]` SaveChanges round-trips end-to-end.
252252

253+
### Variables: `DECLARE` / `SET` / `SELECT @v = expr`
254+
Per-batch scalar variables. `DECLARE @v TYPE [= expr] [, @w TYPE [= expr] ...]` registers slots; `SET @v = expr` and `SELECT @v = expr [, @w = expr2 ...]` mutate them. SqlClient parameters seed the same store as if pre-DECLAREd, so a parameter and a DECLARE can't share a name (Msg 134).
255+
256+
Variable references resolve at runtime via a captured `VariableSlot` — required because mutations between statements have to be visible to subsequent reads. Assignment coercion routes through `Cast.ApplyCoercion` so the slot's declared type is honored: `SET @v(varchar(3)) = 'hello'` truncates to `'hel'`; `SET @v(int) = 'abc'` raises Msg 245.
257+
258+
**SELECT-assign quirks**:
259+
- All-or-nothing: `SELECT @v = 1, 2` (mixing assign and projection) → Msg 141.
260+
- Empty result-set keeps prior value (no rows iterate, slot unchanged); `SET @v = (SELECT no rows)` differs — assigns NULL.
261+
- Multi-row last-row-wins post-ORDER-BY (per-row evaluation, last write wins).
262+
- The dispatch drains rows for side-effects and yields a `SimulatedNonQuery` rather than a result set (matches SQL Server's no-result-set-envelope behavior for SELECT-assign).
263+
264+
**Errors**: Msg 137 use-before-declare (existing factory); Msg 134 duplicate DECLARE (also fires for parameter+DECLARE collision); Msg 141 mixed assignment + retrieval; standard CAST errors propagate from coercion (Msg 245, Msg 8115, etc.). `DECLARE @v INT NOT NULL` and `DECLARE @v INT = DEFAULT` raise Msg 102 / 156 respectively (DECLARE doesn't accept column-style constraints — falls out of grammar mismatch).
265+
266+
**Output-parameter write-back**: at end of batch, the dispatch walks the parameter list and copies each `InputOutput` / `Output` direction parameter's final slot value back to `DbParameter.Value`. Mirrors SqlClient's round-trip behavior for hand-rolled scripts that mutate parameters.
267+
268+
**`@@ROWCOUNT`**: tracks the most-recently-completed statement's row count via `Simulation.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.
269+
270+
**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. Statements within a batch require `;` separators (existing simulator-wide convention; real SQL Server accepts implicit separation in many cases).
271+
253272
### Common table expressions
254273
`WITH name [(col, …)] AS (SELECT …) [, …] {SELECT|INSERT|UPDATE|DELETE|MERGE} …`. WITH prefix scopes to exactly one immediately-following statement. Both non-recursive and recursive forms modeled.
255274

@@ -329,6 +348,11 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
329348
- `PRIMARY KEY` / `UNIQUE` on a computed column (`NotSupportedException`).
330349
- Heap allocation tracking (flat page list, no IAM/PFS).
331350
- Per-connection session state for `SCOPE_IDENTITY()` / `@@IDENTITY`, `SET IDENTITY_INSERT`'s active table, `DBCC TRACEON(N)` flags — all live on `Simulation` rather than connection. (Tx state is already per-connection.)
351+
- Compound assignment (`SET @v += expr` / `-=` / `*=` etc.) — rewrite as `SET @v = @v + expr`. The arithmetic-operator runtime is locked behind `protected` instance methods on `TwoSidedExpression`; exposing them as static helpers is the prerequisite refactor.
352+
- Table variables (`DECLARE @t TABLE (...)`) — separate feature with its own storage / scope / lifecycle.
353+
- T-SQL control flow (`IF` / `WHILE` / `BEGIN ... END` / `BREAK` / `CONTINUE`) — Bundle 2 of scripting.
354+
- `TRY ... CATCH`, `THROW`, `RAISERROR`, `@@ERROR`, `RETURN`, `PRINT`, stored procs / UDFs.
355+
- Implicit statement separation: real SQL Server accepts `declare @v int = 7 select @v` without `;`; simulator requires explicit semicolons between statements when one ends with an expression.
332356
- `hierarchyid`, `geography`, `geometry`.
333357

334358
## Quirks (modeled, not byte-identical to SQL Server)

SqlServerSimulator.Tests.EFCore/EFCoreFromSql.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ public void FromSqlInterpolated_StringParameterAgainstIntColumn_PromotesAndMatch
4444
Assert.AreEqual(2, customer.Id);
4545
Assert.AreEqual("beta", customer.Name);
4646
}
47+
4748
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
using static SqlServerSimulator.TestHelpers;
4+
5+
namespace SqlServerSimulator;
6+
7+
[TestClass]
8+
public sealed class VariableTests
9+
{
10+
[TestMethod]
11+
public void Declare_NoInit_VariableIsNull()
12+
=> AreEqual(DBNull.Value, ExecuteScalar("declare @v int; select @v"));
13+
14+
[TestMethod]
15+
public void Declare_WithInit_HoldsValue()
16+
=> AreEqual(7, ExecuteScalar<int>("declare @v int = 7; select @v"));
17+
18+
[TestMethod]
19+
public void Declare_AsKeywordOptional()
20+
=> AreEqual(1, ExecuteScalar<int>("declare @v as int = 1; select @v"));
21+
22+
[TestMethod]
23+
public void Declare_MultiVariable_AllReadable()
24+
{
25+
using var conn = new Simulation().CreateOpenConnection();
26+
using var cmd = conn.CreateCommand("declare @v int = 7, @w varchar(5) = 'abc'; select @v, @w");
27+
using var reader = cmd.ExecuteReader();
28+
IsTrue(reader.Read());
29+
AreEqual(7, reader.GetInt32(0));
30+
AreEqual("abc", reader.GetString(1));
31+
}
32+
33+
[TestMethod]
34+
public void Set_Assigns()
35+
=> AreEqual(42, ExecuteScalar<int>("declare @v int; set @v = 42; select @v"));
36+
37+
[TestMethod]
38+
public void Set_StringToInt_Coerces()
39+
=> AreEqual(42, ExecuteScalar<int>("declare @v int; set @v = '42'; select @v"));
40+
41+
[TestMethod]
42+
public void Set_BadStringToInt_RaisesMsg245()
43+
=> AssertSqlError("declare @v int; set @v = 'abc'; select @v", 245,
44+
"Conversion failed when converting the varchar value 'abc' to data type int.");
45+
46+
[TestMethod]
47+
public void Set_StringTruncatesToVarcharLength()
48+
=> AreEqual("hel", ExecuteScalar("declare @v varchar(3); set @v = 'hello'; select @v"));
49+
50+
[TestMethod]
51+
public void Set_DecimalToInt_Truncates()
52+
=> AreEqual(3, ExecuteScalar<int>("declare @v int; set @v = 3.7; select @v"));
53+
54+
[TestMethod]
55+
public void Set_ScalarSubquery_Assigns()
56+
=> AreEqual(42, ExecuteScalar<int>("declare @v int; set @v = (select 42); select @v"));
57+
58+
[TestMethod]
59+
public void Set_MultiRowSubquery_RaisesMsg512()
60+
{
61+
var sim = new Simulation();
62+
_ = sim.ExecuteNonQuery("create table t (id int); insert t values (1),(2)");
63+
_ = sim.AssertSqlError("declare @v int; set @v = (select id from t); select @v", 512);
64+
}
65+
66+
[TestMethod]
67+
public void Set_EmptySubquery_AssignsNull()
68+
=> AreEqual(DBNull.Value, ExecuteScalar(
69+
"declare @v int = 99; set @v = (select 1 where 1 = 0); select @v"));
70+
71+
[TestMethod]
72+
public void SelectAssign_Single()
73+
=> AreEqual(42, ExecuteScalar<int>("declare @v int; select @v = 42; select @v"));
74+
75+
[TestMethod]
76+
public void SelectAssign_LastRowWins()
77+
{
78+
using var conn = new Simulation().CreateOpenConnection();
79+
_ = conn.CreateCommand("create table t (id int); insert t values (1),(2),(3)").ExecuteNonQuery();
80+
AreEqual(3, conn.CreateCommand(
81+
"declare @v int; select @v = id from t order by id; select @v").ExecuteScalar());
82+
}
83+
84+
[TestMethod]
85+
public void SelectAssign_EmptyResult_KeepsPriorValue()
86+
{
87+
using var conn = new Simulation().CreateOpenConnection();
88+
_ = conn.CreateCommand("create table t (id int); insert t values (1)").ExecuteNonQuery();
89+
AreEqual(99, conn.CreateCommand(
90+
"declare @v int = 99; select @v = id from t where id = 0; select @v").ExecuteScalar());
91+
}
92+
93+
[TestMethod]
94+
public void SelectAssign_NullResult_AssignsNull()
95+
=> AreEqual(DBNull.Value, ExecuteScalar(
96+
"declare @v int = 99; select @v = case when 1=0 then 1 else null end; select @v"));
97+
98+
[TestMethod]
99+
public void SelectAssign_MultiVariable()
100+
{
101+
using var conn = new Simulation().CreateOpenConnection();
102+
using var cmd = conn.CreateCommand(
103+
"declare @v int, @w varchar(10); select @v = 1, @w = 'x'; select @v, @w");
104+
using var reader = cmd.ExecuteReader();
105+
IsTrue(reader.Read());
106+
AreEqual(1, reader.GetInt32(0));
107+
AreEqual("x", reader.GetString(1));
108+
}
109+
110+
[TestMethod]
111+
public void SelectAssign_MultiVariableLastRowWins()
112+
{
113+
using var conn = new Simulation().CreateOpenConnection();
114+
_ = conn.CreateCommand("create table t (a int, b int); insert t values (1, 2), (3, 4)").ExecuteNonQuery();
115+
using var cmd = conn.CreateCommand(
116+
"declare @v int, @w int; select @v = a, @w = b from t order by a; select @v, @w");
117+
using var reader = cmd.ExecuteReader();
118+
IsTrue(reader.Read());
119+
AreEqual(3, reader.GetInt32(0));
120+
AreEqual(4, reader.GetInt32(1));
121+
}
122+
123+
[TestMethod]
124+
public void SelectAssign_MixedWithProjection_RaisesMsg141()
125+
=> AssertSqlError("declare @v int; select @v = 1, 2", 141,
126+
"A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations.");
127+
128+
[TestMethod]
129+
public void Reference_BeforeDeclare_RaisesMsg137()
130+
=> AssertSqlError("select @x", 137, "Must declare the scalar variable \"@x\".");
131+
132+
[TestMethod]
133+
public void Declare_Duplicate_RaisesMsg134()
134+
=> AssertSqlError("declare @v int; declare @v int; select @v", 134,
135+
"The variable name '@v' has already been declared. Variable names must be unique within a query batch or stored procedure.");
136+
137+
[TestMethod]
138+
public void Declare_NameCollidesWithParameter_RaisesMsg134()
139+
{
140+
using var conn = new Simulation().CreateOpenConnection();
141+
using var cmd = conn.CreateCommand("declare @x int = 99; select @x", ("@x", 1));
142+
var ex = Throws<DbException>(cmd.ExecuteScalar);
143+
AreEqual("134", ex.Data["HelpLink.EvtID"]);
144+
}
145+
146+
[TestMethod]
147+
public void ReservedKeywordAsVariableName_Works()
148+
=> AreEqual(1, ExecuteScalar<int>("declare @select int = 1; select @select"));
149+
150+
[TestMethod]
151+
public void Declare_SelfReference_RaisesMsg137()
152+
=> AssertSqlError("declare @v int = @v + 1; select @v", 137);
153+
154+
[TestMethod]
155+
public void Declare_MultipleStatementsViaSemicolon()
156+
=> AreEqual(3, ExecuteScalar<int>("declare @x int = 1; declare @y int = 2; select @x + @y"));
157+
158+
[TestMethod]
159+
public void Variable_InWhereClause_FiltersRows()
160+
=> IsNull(ExecuteScalar("declare @x int = 5; select 1 where 1 = @x"));
161+
162+
[TestMethod]
163+
public void RowCount_AfterSelect()
164+
{
165+
using var conn = new Simulation().CreateOpenConnection();
166+
using var cmd = conn.CreateCommand(
167+
"select 1 union select 2 union select 3; select @@rowcount as rc");
168+
using var reader = cmd.ExecuteReader();
169+
IsTrue(reader.NextResult());
170+
IsTrue(reader.Read());
171+
AreEqual(3, reader.GetInt32(0));
172+
}
173+
174+
[TestMethod]
175+
public void RowCount_AfterDeclareWithInit_IsOne()
176+
=> AreEqual(1, ExecuteScalar<int>(
177+
"declare @v int = 99; select @@rowcount"));
178+
179+
[TestMethod]
180+
public void RowCount_AfterBareDeclare_PreservedFromPriorStatement()
181+
{
182+
// INSERT sets @@ROWCOUNT to 3; bare DECLARE without init does NOT reset.
183+
using var conn = new Simulation().CreateOpenConnection();
184+
_ = conn.CreateCommand("create table t (id int)").ExecuteNonQuery();
185+
using var cmd = conn.CreateCommand(
186+
"insert t values (1),(2),(3); declare @v int; select @@rowcount");
187+
AreEqual(3, cmd.ExecuteScalar());
188+
}
189+
190+
[TestMethod]
191+
public void RowCount_AfterSet_IsOne()
192+
=> AreEqual(1, ExecuteScalar<int>("declare @v int; set @v = 42; select @@rowcount"));
193+
194+
[TestMethod]
195+
public void OutputParameter_WrittenBackAfterBatch()
196+
{
197+
using var conn = new Simulation().CreateOpenConnection();
198+
using var cmd = conn.CreateCommand("set @x = 999");
199+
var p = cmd.CreateParameter();
200+
p.ParameterName = "@x";
201+
p.DbType = System.Data.DbType.Int32;
202+
p.Direction = System.Data.ParameterDirection.InputOutput;
203+
p.Value = 5;
204+
_ = cmd.Parameters.Add(p);
205+
_ = cmd.ExecuteNonQuery();
206+
AreEqual(999, p.Value);
207+
}
208+
209+
[TestMethod]
210+
public void InputParameter_NotWrittenBack()
211+
{
212+
using var conn = new Simulation().CreateOpenConnection();
213+
using var cmd = conn.CreateCommand("set @x = 999");
214+
var p = cmd.CreateParameter();
215+
p.ParameterName = "@x";
216+
p.DbType = System.Data.DbType.Int32;
217+
p.Direction = System.Data.ParameterDirection.Input;
218+
p.Value = 5;
219+
_ = cmd.Parameters.Add(p);
220+
_ = cmd.ExecuteNonQuery();
221+
AreEqual(5, p.Value);
222+
}
223+
}

SqlServerSimulator/Errors/SimulatedSqlException.ResolutionErrors.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ internal static SimulatedSqlException AmbiguousColumnName(string name) =>
2525

2626
internal static SimulatedSqlException MustDeclareScalarVariable(string name) => new($"Must declare the scalar variable \"@{name}\".", 137, 15, 2);
2727

28+
/// <summary>
29+
/// Mimics SQL Server's Msg 134 — fired when a <c>DECLARE</c> names a
30+
/// variable that already exists in the batch (either a previous
31+
/// <c>DECLARE</c> or a SqlClient parameter of the same name —
32+
/// probe-confirmed parameters and declared variables share a
33+
/// namespace).
34+
/// </summary>
35+
internal static SimulatedSqlException VariableAlreadyDeclared(string name) =>
36+
new($"The variable name '@{name}' has already been declared. Variable names must be unique within a query batch or stored procedure.", 134, 15, 1);
37+
38+
/// <summary>
39+
/// Mimics SQL Server's Msg 141 — fired when a <c>SELECT</c> mixes
40+
/// variable assignment (<c>@v = expr</c>) with non-assignment
41+
/// projection elements in the same projection list.
42+
/// </summary>
43+
internal static SimulatedSqlException SelectAssignmentMixedWithRetrieval() =>
44+
new("A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations.", 141, 15, 1);
45+
2846
/// <summary>
2947
/// Mimics SQL Server error 4104: the OUTPUT clause references an
3048
/// identifier that doesn't exist in either the INSERTED/DELETED virtual

SqlServerSimulator/Parser/AtAtKeyword.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum AtAtKeyword
1313
NestLevel,
1414
Options,
1515
RemServer,
16+
RowCount,
1617
ServerName,
1718
ServiceName,
1819
SpId,

SqlServerSimulator/Parser/Expression.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ public static Expression Parse(ParserContext context)
6161
{
6262
Numeric number => new Value(number.Value),
6363
Literal literal => new Value(literal.Value),
64-
AtPrefixedString atPrefixed => new Value(atPrefixed, context),
64+
AtPrefixedString atPrefixed => new VariableReference(atPrefixed, context),
6565
DoubleAtPrefixedString doubleAtPrefixedString => doubleAtPrefixedString.Parse() switch
6666
{
6767
AtAtKeyword.Identity => new LastIdentityExpression(context.Simulation),
6868
AtAtKeyword.TranCount => new TranCountExpression(context),
69+
AtAtKeyword.RowCount => new RowCountExpression(context.Simulation),
6970
_ => new Value(doubleAtPrefixedString),
7071
},
7172
ReservedKeyword { Keyword: Keyword.Null } => new Value(),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator.Parser.Expressions;
4+
5+
/// <summary>
6+
/// Represents a <c>@v = expr</c> projection element in a SELECT-assign:
7+
/// holds the live <see cref="VariableSlot"/> reference (mutated as the
8+
/// projection runs row-by-row) and the RHS source expression. <c>Run</c>
9+
/// has the side effect of writing to the slot via the standard CAST
10+
/// coercion path; the returned <see cref="SqlValue"/> is the post-coerce
11+
/// value but is never surfaced because SELECT-assign produces no result
12+
/// rows.
13+
/// </summary>
14+
/// <remarks>
15+
/// Empty-result-keeps-prior-value (probe-confirmed) falls out naturally:
16+
/// when the FROM clause yields zero rows, this expression's <c>Run</c>
17+
/// is never called, so the slot retains its prior value. Non-empty
18+
/// last-row-wins (also probe-confirmed) follows from per-row evaluation
19+
/// — each row's <c>Run</c> overwrites the slot, so the final value is the
20+
/// last iterated row's RHS.
21+
/// </remarks>
22+
internal sealed class AssignmentExpression(VariableSlot slot, Expression source) : Expression
23+
{
24+
public readonly VariableSlot Slot = slot;
25+
26+
public readonly Expression Source = source;
27+
28+
public override SqlValue Run(Func<MultiPartName, SqlValue> getColumnValue)
29+
{
30+
var value = this.Source.Run(getColumnValue);
31+
var coerced = Cast.ApplyCoercion(value, this.Slot.DeclaredType, this.Slot.DeclaredMaxLength);
32+
this.Slot.Value = coerced;
33+
return coerced;
34+
}
35+
36+
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => this.Slot.DeclaredType;
37+
38+
internal override string DebugDisplay() => $"@{this.Slot.DeclaredType} = {this.Source.DebugDisplay()}";
39+
}

SqlServerSimulator/Parser/Expressions/AtTimeZone.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public static AtTimeZone ParsePostfix(Expression source, ParserContext context)
178178
Literal lit => new Value(lit.Value),
179179
Numeric num => new Value(num.Value),
180180
ReservedKeyword { Keyword: Keyword.Null } => new Value(),
181-
AtPrefixedString atVar => new Value(atVar, context),
181+
AtPrefixedString atVar => new VariableReference(atVar, context),
182182
Name name => new Reference(name),
183183
Operator { Character: '(' } => ParseParenthesizedZone(context),
184184
_ => throw SimulatedSqlException.SyntaxErrorNear(context),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator.Parser.Expressions;
4+
5+
/// <summary>
6+
/// Backs <c>@@ROWCOUNT</c>: returns the row count of the most recently
7+
/// completed statement in the current batch as <see cref="SqlType.Int32"/>.
8+
/// Probe-confirmed semantics (against SQL Server 2025, 2026-05-12):
9+
/// <list type="bullet">
10+
/// <item>SELECT result count (rows produced).</item>
11+
/// <item>INSERT / UPDATE / DELETE / MERGE rows-affected.</item>
12+
/// <item><c>SET @v = expr</c> and <c>DECLARE @v T = init</c> set it to 1.</item>
13+
/// <item><c>DECLARE @v T</c> (no initializer) does NOT reset it.</item>
14+
/// <item>SELECT-assign with FROM sets it to rows scanned (regardless of
15+
/// whether assignments fired); empty FROM result sets it to 0.</item>
16+
/// <item>Most other statements (PRINT, BEGIN, COMMIT, etc.) reset to 0.</item>
17+
/// </list>
18+
/// </summary>
19+
internal sealed class RowCountExpression(Simulation simulation) : Expression
20+
{
21+
public override SqlValue Run(Func<MultiPartName, SqlValue> getColumnValue) =>
22+
SqlValue.FromInt32(simulation.LastStatementRowCount);
23+
24+
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Int32;
25+
26+
internal override string DebugDisplay() => "@@ROWCOUNT";
27+
}

0 commit comments

Comments
 (0)