Skip to content

Commit 451e64a

Browse files
committed
System statistical DBA-introspection surface ships: the @@ activity counters (@@CPU_BUSY/@@IDLE/@@IO_BUSY/@@PACK_RECEIVED/@@PACK_SENT/@@PACKET_ERRORS/@@TOTAL_ERRORS/@@TOTAL_READ/@@TOTAL_WRITE) report constant 0 as int, @@timeticks reports real's hardware-invariant 31250, @@connections reports the live session-allocation count derived from the SPID allocator, and sys.fn_virtualfilestats mirrors real's 12-column shape with NULL-wildcard/zero-rows-on-miss semantics, one row per database at file_id 1 since there is no physical file model.
1 parent 61be39c commit 451e64a

11 files changed

Lines changed: 392 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ Comparison (Msg 402), ORDER BY/DISTINCT (Msg 306), and aggregates (Msg 8117 from
225225
Per-feature deep-dives live under `docs/claude/`.
226226
Each entry below is a trigger: read the linked file on demand when working in the matching area.
227227

228-
- **Built-in scalars** — math; date (DATETRUNC / DATE_BUCKET / SWITCHOFFSET / TODATETIMEOFFSET / `*FROMPARTS` / AT TIME ZONE / current-time); string (CONCAT / SOUNDEX / TRANSLATE / STRING_ESCAPE / DIFFERENCE); CHOOSE / IIF; bit (BIT_COUNT / GET_BIT / SET_BIT / shifts); CHECKSUM / BINARY_CHECKSUM; FORMAT / FORMATMESSAGE; RAND; STRING_SPLIT / GENERATE_SERIES; COMPRESS / DECOMPRESS; PWDENCRYPT / PWDCOMPARE / LOGINPROPERTY; sys.fn_varbintohexsubstring / sys.fn_varbintohexstr (varbinary→hex, sys-qualified system functions); `@@`-constants + HOST_NAME / APP_NAME / GETANSINULL / ORIGINAL_DB_NAME; session-state (SESSION_CONTEXT / sp_set_session_context / CONTEXT_INFO / CONNECTIONPROPERTY / SESSIONPROPERTY); SQL_VARIANT_PROPERTY → [`scalars.md`](docs/claude/scalars.md).
228+
- **Built-in scalars** — math; date (DATETRUNC / DATE_BUCKET / SWITCHOFFSET / TODATETIMEOFFSET / `*FROMPARTS` / AT TIME ZONE / current-time); string (CONCAT / SOUNDEX / TRANSLATE / STRING_ESCAPE / DIFFERENCE); CHOOSE / IIF; bit (BIT_COUNT / GET_BIT / SET_BIT / shifts); CHECKSUM / BINARY_CHECKSUM; FORMAT / FORMATMESSAGE; RAND; STRING_SPLIT / GENERATE_SERIES; COMPRESS / DECOMPRESS; PWDENCRYPT / PWDCOMPARE / LOGINPROPERTY; sys.fn_varbintohexsubstring / sys.fn_varbintohexstr (varbinary→hex, sys-qualified system functions); `@@`-constants (incl. the system statistical counters + `sys.fn_virtualfilestats`) + HOST_NAME / APP_NAME / GETANSINULL / ORIGINAL_DB_NAME; session-state (SESSION_CONTEXT / sp_set_session_context / CONTEXT_INFO / CONNECTIONPROPERTY / SESSIONPROPERTY); SQL_VARIANT_PROPERTY → [`scalars.md`](docs/claude/scalars.md).
229229
- **`SqlType.Promote` / `PromoteForArithmetic` / decimal precision-scale / int↔string promotion / string+binary literal value-width typing + width algebra (concat sum-cap, CASE/COALESCE/set-op max, per-function widths)**[`arithmetic.md`](docs/claude/arithmetic.md).
230230
- **`Cast` / coercion error paths** (CAST/CONVERT narrow targets, TRY_CAST/TRY_CONVERT swallow set, PARSE/TRY_PARSE culture-aware parsing) → [`casting.md`](docs/claude/casting.md).
231231
- **`SimulatedDbDataReader` client surface** (typed accessors, `datetime` client-millisecond rounding, `GetOrdinal` precedence, GetBytes/GetChars materialization divergence) → [`data-reader.md`](docs/claude/data-reader.md).

docs/claude/backlog.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ Blocked on a larger unmodeled parent feature (shipping a function here implies t
6161
Low priority / niche — simulatable (as placeholder constants or a small model) but rarely hit, so not worth attention yet:
6262

6363
- **Legacy text/image** — TEXTPTR / TEXTVALID (the `text` / `ntext` / `image` types ship; these navigate the deprecated READTEXT / WRITETEXT / UPDATETEXT pointer path).
64-
- **System statistical** (DBA introspection) — @@CONNECTIONS / @@CPU_BUSY / @@IDLE / @@IO_BUSY / @@PACK_RECEIVED / @@PACK_SENT / @@PACKET_ERRORS / @@TIMETICKS / @@TOTAL_ERRORS / @@TOTAL_READ / @@TOTAL_WRITE / fn_virtualfilestats.
65-
Plausible constants would satisfy most callers; rarely hit from app code.
6664
- **Files / filegroups** — FILE_ID / FILE_IDEX / FILE_NAME / FILEGROUP_ID / FILEGROUP_NAME / FILEGROUPPROPERTY / FILEPROPERTY.
6765
No physical file model.
6866
A named-filegroup registry now ships (`Database.Filegroups`, seeded PRIMARY=1, extended by the bacpac loader's `SqlFilegroup` dispatch; surfaced by `sys.filegroups`/`sys.data_spaces` and FILEGROUP-scoped extended properties) — the scalar helpers above could read it, though a `file_id` 1 placeholder is still needed for the file-level ones.

docs/claude/scalars.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,29 @@ Constants whose values don't carry real session/server identity in the simulator
411411

412412
Server-instance metadata accessed via **`SERVERPROPERTY(name)`** — see [`catalog-views.md`](catalog-views.md).
413413

414+
## System statistical counters + `sys.fn_virtualfilestats`
415+
416+
DBA-introspection surface for cumulative server activity.
417+
Every `@@`-counter is **`int`** (probe-confirmed against SQL Server 2025, 2026-07-19); real reports live totals since server start, but the in-process simulator performs no physical IO, CPU-time accounting, or TDS-packet counting, so the elapsed-activity totals report a plausible **0** (the honest reading for a freshly started, idle instance).
418+
Rarely read from application code; the constants exist so DBA tooling / health scripts receive a sensible non-error response.
419+
420+
- **`@@CPU_BUSY`** / **`@@IDLE`** / **`@@IO_BUSY`** — 0 (no CPU-time / idle-time / IO-time accounting).
421+
- **`@@PACK_RECEIVED`** / **`@@PACK_SENT`** — 0 (no TDS packet counting, even on the network endpoint).
422+
- **`@@PACKET_ERRORS`** / **`@@TOTAL_ERRORS`** — 0 (also 0 on a healthy real server).
423+
- **`@@TOTAL_READ`** / **`@@TOTAL_WRITE`** — 0 (no physical disk-read / -write accounting).
424+
- **`@@TIMETICKS`****31250**, the hardware-invariant microseconds-per-tick constant real reports (Value-constant form).
425+
- **`@@CONNECTIONS`** — live signal, unlike the frozen constants above: a dedicated `ConnectionsExpression` reads `Simulation.ConnectionsAllocated` (the SPID allocator's distance past its seed of 50), so it advances on every session without separate instrumentation.
426+
Real reports cumulative login attempts since server start; the session-allocation count is the closest cheap in-process proxy.
427+
A fresh `Simulation`'s first connection reads 1.
428+
429+
**`sys.fn_virtualfilestats(database_id, file_id)`** — system TVF, invoked bare (`fn_virtualfilestats(...)`) or `sys.`-qualified.
430+
Column shape mirrors real exactly: `(DbId smallint, FileId smallint, TimeStamp bigint, NumberReads bigint, BytesRead bigint, IoStallReadMS bigint, NumberWrites bigint, BytesWritten bigint, IoStallWriteMS bigint, IoStallMS bigint, BytesOnDisk bigint, FileHandle varbinary(8))`.
431+
`NULL` is the wildcard at either argument (all databases / all files); a non-NULL id naming no database or file yields zero rows (including negatives such as `-1`), matching real.
432+
The simulator has no physical file model, so it reports **one row per (database, `file_id 1`)** with every IO counter, `BytesOnDisk`, and `FileHandle` at 0 — the file cardinality per database differs from real (real seeds tempdb with multiple data files) but the wildcard / filter semantics and column shape match.
433+
Wrong arg count matches real: one argument → **Msg 313** (insufficient arguments), three → **Msg 8144** (too many arguments).
434+
The legacy `::fn_virtualfilestats(...)` prefix form isn't tokenized (`::` needs grammar work); the bare and `sys.`-qualified forms cover the documented invocations.
435+
Dispatch lives in `Parser/Selection.VirtualFileStats.cs`, wired into `ParseSingleFromSourceCore` after `ParseObjectName` (so the `sys.` qualifier parses first, unlike the qualifier-less rowset functions).
436+
414437
## Session-state store: `SESSION_CONTEXT` / `CONTEXT_INFO` / connection scalars
415438

416439
These carry real per-session state on `SimulatedDbConnection` (not placeholder constants), so values persist across batches on the same connection and reset with a new connection.

src/SqlServerSimulator/Parser/AtAtKeyword.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
enum AtAtKeyword
44
{
55
_ = 0, // "Default" value for the enum, indicating not a keyword.
6+
Connections,
7+
CpuBusy,
68
CursorRows,
79
DateFirst,
810
Dbts,
911
Error,
1012
FetchStatus,
1113
Identity,
14+
Idle,
15+
IoBusy,
1216
LangId,
1317
Language,
1418
LockTimeout,
@@ -17,13 +21,20 @@ enum AtAtKeyword
1721
MicrosoftVersion,
1822
NestLevel,
1923
Options,
24+
PackReceived,
25+
PacketErrors,
26+
PackSent,
2027
ProcId,
2128
RemServer,
2229
RowCount,
2330
ServerName,
2431
ServiceName,
2532
SpId,
2633
TextSize,
34+
TimeTicks,
35+
TotalErrors,
36+
TotalRead,
37+
TotalWrite,
2738
TranCount,
2839
Version
2940
}

src/SqlServerSimulator/Parser/Expression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ internal static Expression ParsePrimary(ParserContext context)
171171
AtPrefixedString atPrefixed => new VariableReference(atPrefixed, context),
172172
DoubleAtPrefixedString doubleAtPrefixedString => doubleAtPrefixedString.Parse() switch
173173
{
174+
AtAtKeyword.Connections => new ConnectionsExpression(),
174175
AtAtKeyword.Error => new LastErrorExpression(),
175176
AtAtKeyword.Identity => new LastIdentityExpression(),
176177
AtAtKeyword.TranCount => new TranCountExpression(context),

src/SqlServerSimulator/Parser/Expressions/SessionAtAtExpressions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
namespace SqlServerSimulator.Parser.Expressions;
44

5+
/// <summary>
6+
/// Backs <c>@@CONNECTIONS</c>: returns the number of sessions the
7+
/// <see cref="Simulation"/> has allocated as <see cref="SqlType.Int32"/>
8+
/// (real SQL Server's @@CONNECTIONS is <c>int</c> — probe-confirmed). Reads
9+
/// <see cref="Simulation.ConnectionsAllocated"/>, a live count derived from
10+
/// the SPID allocator; on real SQL Server this is cumulative login attempts
11+
/// since server start, which the session-allocation count proxies without
12+
/// separate instrumentation.
13+
/// </summary>
14+
internal sealed class ConnectionsExpression : Expression
15+
{
16+
public override SqlValue Run(RuntimeContext runtime) =>
17+
SqlValue.FromInt32(runtime.Batch.Connection.Simulation.ConnectionsAllocated);
18+
19+
public override SqlType GetSqlType(BatchContext batch, Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Int32;
20+
21+
internal override string DebugDisplay() => "@@CONNECTIONS";
22+
}
23+
524
/// <summary>
625
/// Backs <c>@@NESTLEVEL</c>: returns the connection's current nesting
726
/// depth (the count of active procedure/UDF/trigger frames) as

src/SqlServerSimulator/Parser/Expressions/Value.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,29 @@ public Value(DoubleAtPrefixedString doubleAtPrefixedString)
6161
case AtAtKeyword.DateFirst:
6262
this.Constant = SqlValue.FromByte(7);
6363
return;
64+
// System statistical counters (all int, probe-confirmed
65+
// 2026-07-19 against SQL Server 2025). The in-process simulator
66+
// does no physical IO, CPU-time accounting, or TDS packet
67+
// counting, so the elapsed-activity totals report 0 — the honest
68+
// reading for a freshly started, idle instance. @@PACKET_ERRORS
69+
// and @@TOTAL_ERRORS report 0 on a healthy real server too.
70+
// @@CONNECTIONS routes to a dedicated runtime expression (it
71+
// reflects the live session-allocation count). @@TIMETICKS is the
72+
// hardware-invariant microseconds-per-tick constant real reports.
73+
case AtAtKeyword.CpuBusy:
74+
case AtAtKeyword.Idle:
75+
case AtAtKeyword.IoBusy:
76+
case AtAtKeyword.PackReceived:
77+
case AtAtKeyword.PacketErrors:
78+
case AtAtKeyword.PackSent:
79+
case AtAtKeyword.TotalErrors:
80+
case AtAtKeyword.TotalRead:
81+
case AtAtKeyword.TotalWrite:
82+
this.Constant = SqlValue.FromInt32(0);
83+
return;
84+
case AtAtKeyword.TimeTicks:
85+
this.Constant = SqlValue.FromInt32(31250);
86+
return;
6487
}
6588

6689
throw new NotSupportedException($"Simulator doesn't recognize {doubleAtPrefixedString}.");
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using SqlServerSimulator.Parser.Expressions;
2+
using SqlServerSimulator.Parser.Tokens;
3+
using SqlServerSimulator.Storage;
4+
5+
namespace SqlServerSimulator.Parser;
6+
7+
partial class Selection
8+
{
9+
/// <summary>
10+
/// Built-in system TVF <c>fn_virtualfilestats</c> (invoked bare or
11+
/// <c>sys.</c>-qualified). Two args <c>(database_id, file_id)</c>, both
12+
/// nullable; <c>NULL</c> is the wildcard (all databases / all files),
13+
/// probe-confirmed against SQL Server 2025 (2026-07-19). A non-NULL id
14+
/// that names no database / file yields zero rows (including negative
15+
/// ids such as <c>-1</c>). Column shape mirrors real:
16+
/// <c>(DbId smallint, FileId smallint, TimeStamp bigint, NumberReads
17+
/// bigint, BytesRead bigint, IoStallReadMS bigint, NumberWrites bigint,
18+
/// BytesWritten bigint, IoStallWriteMS bigint, IoStallMS bigint,
19+
/// BytesOnDisk bigint, FileHandle varbinary(8))</c>.
20+
/// </summary>
21+
/// <remarks>
22+
/// The simulator has no physical file model, so it reports one row per
23+
/// (database, <c>file_id 1</c>) with all IO counters and <c>BytesOnDisk</c>
24+
/// at 0 and an all-zero <c>FileHandle</c> — the honest reading for an
25+
/// in-process store that performs no disk IO. The wildcard / filter
26+
/// semantics still match real so a caller enumerating files gets the right
27+
/// row cardinality per database. The legacy <c>::fn_virtualfilestats(...)</c>
28+
/// prefix form isn't tokenized; the bare and <c>sys.</c>-qualified forms
29+
/// cover the documented invocations.
30+
/// </remarks>
31+
public static Selection ParseVirtualFileStats(ParserContext context, string functionName)
32+
{
33+
// On entry the cursor rests on the function name's leaf segment
34+
// (ParseObjectName leaves Token on the leaf); advance to the '('.
35+
if (context.GetNextRequired() is not Operator { Character: '(' })
36+
throw SimulatedSqlException.SyntaxErrorNear(context);
37+
38+
context.MoveNextRequired();
39+
var dbArg = Expression.Parse(context);
40+
41+
if (context.Token is Operator { Character: ')' })
42+
throw SimulatedSqlException.InsufficientArgumentsToFunction(functionName);
43+
if (context.Token is not Operator { Character: ',' })
44+
throw SimulatedSqlException.SyntaxErrorNear(context);
45+
46+
context.MoveNextRequired();
47+
var fileArg = Expression.Parse(context);
48+
49+
// A trailing comma means a third argument was supplied.
50+
if (context.Token is Operator { Character: ',' })
51+
throw SimulatedSqlException.TooManyArgumentsToFunction(functionName);
52+
if (context.Token is not Operator { Character: ')' })
53+
throw SimulatedSqlException.SyntaxErrorNear(context);
54+
context.MoveNextOptional();
55+
56+
SqlType[] schema =
57+
[
58+
SqlType.SmallInt, SqlType.SmallInt, SqlType.BigInt, SqlType.BigInt,
59+
SqlType.BigInt, SqlType.BigInt, SqlType.BigInt, SqlType.BigInt,
60+
SqlType.BigInt, SqlType.BigInt, SqlType.BigInt, VarbinarySqlType.Get(8),
61+
];
62+
string[] columnNames =
63+
[
64+
"DbId", "FileId", "TimeStamp", "NumberReads", "BytesRead", "IoStallReadMS",
65+
"NumberWrites", "BytesWritten", "IoStallWriteMS", "IoStallMS", "BytesOnDisk", "FileHandle",
66+
];
67+
68+
return new Selection(schema, columnNames,
69+
hasOrderBy: false,
70+
hasTopOrOffsetOrFetch: false,
71+
(batch, outerResolver) => EnumerateVirtualFileStatsRows(schema, dbArg, fileArg, batch, outerResolver));
72+
}
73+
74+
private static IEnumerable<byte[]> EnumerateVirtualFileStatsRows(
75+
SqlType[] schema,
76+
Expression dbExpr,
77+
Expression fileExpr,
78+
BatchContext batch,
79+
Func<MultiPartName, SqlValue>? outerResolver)
80+
{
81+
var resolver = outerResolver ?? (n => throw SimulatedSqlException.InvalidColumnName(n));
82+
var runtime = new RuntimeContext(resolver, batch);
83+
var dbFilter = EvalNullableInt(dbExpr, runtime);
84+
var fileFilter = EvalNullableInt(fileExpr, runtime);
85+
86+
// Only file_id 1 is modeled; a non-NULL file argument naming any other
87+
// file yields no rows (NULL is the all-files wildcard).
88+
if (fileFilter is { } wantFile && wantFile != 1)
89+
yield break;
90+
91+
var fileHandle = new byte[8];
92+
foreach (var (_, id) in DbId.DatabasesWithIds(batch.Connection.Simulation))
93+
{
94+
if (dbFilter is { } wantDb && wantDb != id)
95+
continue;
96+
97+
yield return RowEncoder.EncodeRow(schema,
98+
[
99+
SqlValue.FromInt16(id),
100+
SqlValue.FromInt16(1),
101+
SqlValue.FromInt64(0),
102+
SqlValue.FromInt64(0),
103+
SqlValue.FromInt64(0),
104+
SqlValue.FromInt64(0),
105+
SqlValue.FromInt64(0),
106+
SqlValue.FromInt64(0),
107+
SqlValue.FromInt64(0),
108+
SqlValue.FromInt64(0),
109+
SqlValue.FromInt64(0),
110+
SqlValue.FromVarbinary(VarbinarySqlType.Get(8), fileHandle),
111+
]);
112+
}
113+
}
114+
115+
private static int? EvalNullableInt(Expression expr, RuntimeContext runtime)
116+
{
117+
var value = expr.Run(runtime);
118+
return value.IsNull ? null : value.CoerceTo(SqlType.Int32).AsInt32;
119+
}
120+
}

src/SqlServerSimulator/Parser/Selection.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,18 @@ private static FromSource ParseSingleFromSourceCore(ParserContext context, uint
13901390
AfterBuiltInRowsetDispatch:
13911391
var objectName = BatchContext.ParseObjectName(context);
13921392

1393+
// fn_virtualfilestats: a 2-arg system TVF invoked bare or
1394+
// `sys.`-qualified. Handled after ParseObjectName (unlike the
1395+
// 1-part rowset functions above) precisely because it accepts
1396+
// the `sys.` schema qualifier, so the 2-part name must be
1397+
// parsed first. Wins over catalog-view / table lookup.
1398+
if (BuiltInToken.Equals(objectName.Leaf, "fn_virtualfilestats")
1399+
&& (objectName.Count == 1
1400+
|| (objectName.Count == 2 && BuiltInToken.Equals(objectName.ImmediateQualifier, "sys"))))
1401+
{
1402+
return BuiltInRowsetSource(context, ParseVirtualFileStats(context, objectName.ToString()));
1403+
}
1404+
13931405
// Linked-server fork: four-part `server.db.schema.t` routes
13941406
// to the matching <see cref="LinkedServer"/>'s remote
13951407
// <see cref="HeapTable"/>. The lateral plan opens a fresh

src/SqlServerSimulator/Simulation/Simulation.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,16 @@ private sealed class PlanCacheEntry(Selection plan, long schemaVersionAtParse)
603603
/// </summary>
604604
internal int AllocateSpid() => Interlocked.Increment(ref this.nextSpid);
605605

606+
/// <summary>
607+
/// Backs <c>@@CONNECTIONS</c>: the count of sessions allocated since the
608+
/// <see cref="Simulation"/> was constructed. Derived from the SPID
609+
/// allocator's distance past its seed (50), so it advances on every
610+
/// <see cref="SimulatedDbConnection"/> without a separate counter. Real
611+
/// SQL Server reports cumulative login attempts since server start; the
612+
/// session-allocation count is the closest cheap in-process proxy.
613+
/// </summary>
614+
internal int ConnectionsAllocated => Volatile.Read(ref this.nextSpid) - 50;
615+
606616
/// <summary>
607617
/// Monotonic counter for <see cref="GenerateNewSequentialId"/>; each call
608618
/// reserves the next value via <see cref="Interlocked.Increment(ref long)"/>

0 commit comments

Comments
 (0)