Skip to content

Commit b15b721

Browse files
committed
Sequence objects: CREATE / ALTER / DROP SEQUENCE + NEXT VALUE FOR + sys.sequences, including the per-row dedup semantic SQL Server enforces for "multiple NEXT VALUE FOR same-sequence within one statement return the same value per row processed" — implemented via a BatchContext.CurrentRowStamp monotonic counter that the per-row iterators (INSERT VALUES, INSERT destination, SELECT projection streaming/buffered, UPDATE single-table/joined, dispatch loop per statement) bump at each boundary, paired with a Dictionary<Sequence, (stamp, value)> SequenceRowCache that NextValueFor.Run checks before advancing. CREATE SEQUENCE accepts AS type / START WITH / INCREMENT BY / MIN/MAXVALUE / CYCLE / NO CYCLE / CACHE n / NO CACHE; types restricted to tinyint / smallint / int / bigint / decimal(p,0) (Msg 11702 for everything else, including non-zero decimal scale and float/real). ALTER SEQUENCE supports RESTART [WITH n] / INCREMENT BY / MIN/MAXVALUE / CYCLE. Parse-time restricted-context enforcement (Msg 11720) for WHERE / GROUP BY / HAVING / ORDER BY via a new ParserContext.RejectNextValueFor flag; ON / OUTPUT / TOP gaps documented. NEXT VALUE FOR resolves through new BatchContext.TryResolveSequence (1-/2-/3-part name, dbo fallback), with Msg 11726 distinguishing "exists but isn't a sequence" from Msg 208 unknown-name. Storage lives in per-schema Schema.Sequences alongside Tables / Views / Functions / Procs / TableTypes. sys.sequences ships with 14 columns (cache_size always NULL since the simulator doesn't model batched allocation). EF Core 10's .UseHiLo("seqname") works end-to-end against the simulator — new EFCoreHiLo test validates SaveChanges → SELECT NEXT VALUE FOR → sequential client-side ID allocation through the LINQ pipeline. 30 new SequenceTests cover all 8 error paths + cycle behavior + dedup across columns/rows + sys.sequences shape. New docs/claude/sequences.md plus CLAUDE.md updates (Schema bullet, Feature reference, "Not modeled" → shipped).
1 parent 0e9adec commit b15b721

22 files changed

Lines changed: 1436 additions & 8 deletions

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Five scopes, one home each. **Add new state to whichever class matches its true
6262

6363
- **`Simulation`** = server / instance. Process-shared system tables (`SystemHeapTables`), `NEWSEQUENTIALID` anchor + counter, the `Databases` dictionary. Public surface (`Simulation` ctor + `CreateDbConnection()`) stays on this class.
6464
- **`Database`** (internal) = one database hosted by the server instance. `Schemas` (named-schema dict, pre-seeded with `dbo`), `CompatibilityLevel`, `VerboseTruncationWarnings`, `rowVersionCounter` (per-DB `@@DBTS`). Every `Simulation` ships with one entry named `Simulation.DefaultDatabaseName` (`"simulated"`); a future `USE <db>` adds entries to the dictionary.
65-
- **`Schema`** (internal) = one namespace inside a database. `HeapTables` (per-schema table dict), `Functions` (UDFs — abstract `UserDefinedFunction` keyed by name, runtime-typed as either `ScalarFunction` or `InlineTableValuedFunction`), `Views` (per-schema view dict), `Procedures` (per-schema stored-procedure dict), `TableTypes` (per-schema user-defined-table-type dict — `CREATE TYPE … AS TABLE` populates; separate namespace from the object dicts, see `docs/claude/table-valued-parameters.md`). Future sequences / triggers land here too. Schema-qualified references (`SELECT * FROM audit.t`, `SELECT audit.fn(x)`, `FROM audit.tvf(x)`, `FROM audit.view1`, `EXEC audit.proc1`, `DECLARE @t audit.MyType`) route through `Database.Schemas["audit"]`; unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`).
65+
- **`Schema`** (internal) = one namespace inside a database. `HeapTables` (per-schema table dict), `Functions` (UDFs — abstract `UserDefinedFunction` keyed by name, runtime-typed as either `ScalarFunction` or `InlineTableValuedFunction`), `Views` (per-schema view dict), `Procedures` (per-schema stored-procedure dict), `TableTypes` (per-schema user-defined-table-type dict — `CREATE TYPE … AS TABLE` populates; separate namespace from the object dicts, see `docs/claude/table-valued-parameters.md`), `Sequences` (per-schema sequence-object dict — `CREATE SEQUENCE` populates, shares the object namespace with tables/views/funcs/procs, see `docs/claude/sequences.md`). Future triggers land here too. Schema-qualified references (`SELECT * FROM audit.t`, `SELECT audit.fn(x)`, `FROM audit.tvf(x)`, `FROM audit.view1`, `EXEC audit.proc1`, `DECLARE @t audit.MyType`, `NEXT VALUE FOR audit.seq`) route through `Database.Schemas["audit"]`; unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`).
6666
- **`SimulatedDbConnection`** = session. `CurrentDatabase` pointer, `CurrentTransaction`, `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `LastStatementRowCount` (`@@ROWCOUNT`), `LastErrorNumber` (`@@ERROR`), `NestingLevel` (UDF / proc / trigger / view recursion depth, capped at 32), `IdentityInsertTable`, `TraceFlags`, `IsVerboseTruncationActive()`, `TempTables` (per-session `#foo` dictionary, cleared on `Dispose`).
6767
- **`BatchContext`** (internal, in `Parser/`) = one command execution. Owns the `ParserContext` (parse-time-only scratch — `Token`, `AggregateCollector`, `WindowCollector`, `OuterTypeResolver`, `CteBindings`, `InDefaultClause`, `AllowsWindowExpressions`) and holds batch-lifetime runtime state: `Variables`, `TableVariables` (per-batch `DECLARE @t TABLE`–backed `HeapTable` dict, shared namespace with scalar `Variables` for the Msg-134 uniqueness check), `CurrentUndoLog` (per-tx/per-statement, regular tables), `CurrentTableVarUndoLog` (per-statement-only, `@t` writes — kept disjoint from the tx-scoped log so `ROLLBACK TRAN` skips `@t` while statement-atomic rollback covers it), `UdfFrame` (non-null when this batch is a scalar-UDF body being dispatched — gates value-form `RETURN` and lands the return value for the caller), `ProcFrame` (non-null when this batch is a stored-procedure body — same gate for value-form `RETURN`, plus a return-code slot the caller reads), plus the per-statement frame `CurrentStatement`. Exposes `TryResolveTable(MultiPartName)` — the routing rule that dispatches `#foo` leaves to `Connection.TempTables` regardless of qualifier and `@t` leaves to `TableVariables` (1-part only — `dbo.@t` returns false; the caller raises Msg 102 at parse via `acceptTableVariable` gating); everything else routes through the named schema (or `dbo` for an unqualified reference), with `SystemHeapTables` reachable only as a flat 1-part fallback. `TryResolveFunction(MultiPartName)` resolves 2-/3-part dotted names against the named schema's `Functions` dict; 1-part names return false (real SQL Server rejects bare UDF calls with Msg 195). `TryResolveProcedure(MultiPartName)` resolves through the same schema-lookup path but accepts 1-part names (probe-confirmed: `EXEC p1` finds `dbo.p1`). `TryResolveTableType(MultiPartName)` resolves user-defined table types against the named schema's `TableTypes` dict and falls back to `dbo` for 1-part names (DECLARE @t MyType / TVP-parameter-type lookup). `TryResolveSchema(MultiPartName)` exposes the dict-bearing schema for CREATE / DROP / TRUNCATE / SELECT INTO. `ParseObjectName(ParserContext, bool acceptTableVariable = false)` parses the 1–4-segment dotted form, leaves cursor on the last name segment (standard parser contract), and compresses empty middle segments (so `tempdb..#foo` returns a 2-part name); the `acceptTableVariable` opt-in routes `@t` to a 1-part-with-`@`-prefix leaf for DML / FROM-source sites, and rejects `@t` everywhere else (so ALTER TABLE / DROP TABLE / TRUNCATE / CREATE / SELECT INTO surface Msg 102 matching probe). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`. Scalar-UDF and procedure invocation each allocate a child `BatchContext` via the corresponding body constructor: parameters pre-seed `Variables`, the matching frame is set, the body source text (captured at CREATE FUNCTION / CREATE PROCEDURE time) is re-tokenized through a synthesized `SimulatedDbCommand`, and the same dispatch loop runs the body. UDF bodies discard yielded result sets at the call site (Msg 444 territory); procedure bodies forward them through to the outer caller's iterator.
6868
- **`StatementContext`** (internal, in `Parser/`) = the dispatch loop's per-statement frame. Allocated once per batch and overwritten in place at the top of each iteration; holds `UtcNow` (the per-statement-freeze the time scalars read). Stored-proc EXEC / TRY-CATCH frames slot in here when added.
@@ -148,6 +148,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
148148
- **Touching `#foo` routing, DROP TABLE, TRUNCATE TABLE**[`docs/claude/temp-tables.md`](docs/claude/temp-tables.md).
149149
- **Touching `DECLARE @t TABLE`, table-variable DML routing, `OUTPUT … INTO <target>` (`@t` or regular)**[`docs/claude/table-variables.md`](docs/claude/table-variables.md).
150150
- **Touching `CREATE TYPE … AS TABLE`, `DECLARE @t MyType`, TVP procedure parameters / `READONLY`, or the ADO.NET `DbParameter.TypeName` extension (DataTable / IDataReader as a TVP value source)**[`docs/claude/table-valued-parameters.md`](docs/claude/table-valued-parameters.md).
151+
- **Touching `CREATE SEQUENCE` / `ALTER SEQUENCE` / `DROP SEQUENCE`, `NEXT VALUE FOR`, the per-row dedup mechanism (`BatchContext.CurrentRowStamp` / `SequenceRowCache`), or `sys.sequences`**[`docs/claude/sequences.md`](docs/claude/sequences.md).
151152
- **Adding a new top-level statement parser or changing the dispatch loop's statement-separator rules**[`docs/claude/grammar.md`](docs/claude/grammar.md) + [`docs/claude/control-flow.md`](docs/claude/control-flow.md).
152153

153154
## Not modeled
@@ -175,7 +176,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
175176
- **CREATE SCHEMA's `<schema_element>` greedy form** — real SQL Server consumes trailing CREATE TABLE / VIEW / GRANT as part of the same CREATE SCHEMA statement (and requires CREATE SCHEMA to be the first statement in the batch as a result). The simulator instead dispatches the trailing tokens as their own statements — same end state for the common idiom, but mismatched-grammar trailers (e.g. anything that isn't a recognized statement start) raise `NotSupportedException`.
176177
- **`CREATE SCHEMA sys` / `INFORMATION_SCHEMA`** — raises Msg 2760 (matching real SQL Server). The schemas themselves exist as catalog-view hosts (`select * from sys.tables` / `select * from INFORMATION_SCHEMA.COLUMNS` work); legacy bare 1-part system-table access (`select * from systypes`) also still works.
177178
- T-SQL `GOTO` / labels — `IF` / `BEGIN…END` / `WHILE` / `BREAK` / `CONTINUE` / `RETURN` (bare + UDF-body and stored-procedure-body value form) ship; unconditional jumps don't.
178-
- Multi-statement table-valued functions (`RETURNS @t TABLE (...) AS BEGIN ... END`), CLR functions, triggers, sequences. Scalar UDFs, inline TVFs, views, DML-through-views (single-source updatable shape), stored procedures (including CREATE / ALTER / DROP, EXEC with input/output/default params, `@rc = EXEC` return-code capture, `CommandType.StoredProcedure`, `EXEC (@sql)` and `sp_executesql` dynamic SQL), and user-defined table types + table-valued parameters (CREATE TYPE … AS TABLE / DROP TYPE / DECLARE @t MyType / READONLY proc params / EXEC with TVP arg / ADO.NET Structured parameter via the `DbParameter.TypeName` C# 14 extension property + DataTable/IDataReader sources) ship (see `docs/claude/programmable.md` and `docs/claude/table-valued-parameters.md`). JOIN-view single-base-table UPDATE/DELETE, OUTPUT through views, and multi-source alias-form UPDATE/DELETE through views are deferred (Msg 4405 or `NotSupportedException` at the DML site). `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch. Value-form `RETURN N` is legal inside a scalar-UDF body and a stored-procedure body; bare batch / dynamic-SQL scope raises Msg 178. TRY/CATCH + THROW + RAISERROR (printf-style `%s %d/%i %u %o %x %X %% %ld %I64d` with width / precision / left-align / zero-pad; severity routing 0-10 informational vs 11-18 catchable; `WITH SETERROR`/`NOWAIT` ship, `WITH LOG` uniformly raises Msg 2778; numeric `msg_id` raises Msg 2732 for 50000 / `< 13000` and Msg 18054 otherwise — `sys.messages` registry / `sp_addmessage` not modeled) + live `@@ERROR` + `ERROR_*()` functions ship (see `docs/claude/control-flow.md`).
179+
- Multi-statement table-valued functions (`RETURNS @t TABLE (...) AS BEGIN ... END`), CLR functions, triggers. Scalar UDFs, inline TVFs, views, DML-through-views (single-source updatable shape), stored procedures (including CREATE / ALTER / DROP, EXEC with input/output/default params, `@rc = EXEC` return-code capture, `CommandType.StoredProcedure`, `EXEC (@sql)` and `sp_executesql` dynamic SQL), user-defined table types + table-valued parameters (CREATE TYPE … AS TABLE / DROP TYPE / DECLARE @t MyType / READONLY proc params / EXEC with TVP arg / ADO.NET Structured parameter via the `DbParameter.TypeName` C# 14 extension property + DataTable/IDataReader sources), and sequence objects (CREATE / ALTER / DROP SEQUENCE / NEXT VALUE FOR / sys.sequences — supports EF Core HiLo identity strategy end-to-end) ship (see `docs/claude/programmable.md`, `docs/claude/table-valued-parameters.md`, and `docs/claude/sequences.md`). JOIN-view single-base-table UPDATE/DELETE, OUTPUT through views, and multi-source alias-form UPDATE/DELETE through views are deferred (Msg 4405 or `NotSupportedException` at the DML site). `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch. Value-form `RETURN N` is legal inside a scalar-UDF body and a stored-procedure body; bare batch / dynamic-SQL scope raises Msg 178. TRY/CATCH + THROW + RAISERROR (printf-style `%s %d/%i %u %o %x %X %% %ld %I64d` with width / precision / left-align / zero-pad; severity routing 0-10 informational vs 11-18 catchable; `WITH SETERROR`/`NOWAIT` ship, `WITH LOG` uniformly raises Msg 2778; numeric `msg_id` raises Msg 2732 for 50000 / `< 13000` and Msg 18054 otherwise — `sys.messages` registry / `sp_addmessage` not modeled) + live `@@ERROR` + `ERROR_*()` functions ship (see `docs/claude/control-flow.md`).
179180
- **`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.
180181
- `hierarchyid`, `geography`, `geometry`.
181182

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// EF Core's HiLo identity strategy (<c>.UseHiLo("seqname")</c>) issues
7+
/// SaveChanges-time <c>SELECT NEXT VALUE FOR &lt;seq&gt;</c> calls to allocate
8+
/// IDs in client-side batches. Coverage here boots a sequence object manually
9+
/// (since EF migrations / EnsureCreated isn't exercised by other tests) and
10+
/// then exercises the LINQ→SQL pipeline through SaveChanges — confirms that
11+
/// the simulator's NEXT VALUE FOR shape works against the EF SqlServer
12+
/// provider's emit pattern.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class EFCoreHiLo
16+
{
17+
public TestContext TestContext { get; set; } = null!;
18+
19+
private class HiLoEntity
20+
{
21+
public int Id { get; set; }
22+
public string Name { get; set; } = null!;
23+
}
24+
25+
private class HiLoDbContext(Simulation simulation) : DbContext
26+
{
27+
public Simulation Simulation { get; } = simulation;
28+
29+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
30+
{
31+
_ = optionsBuilder.UseSqlServer(this.Simulation.CreateDbConnection());
32+
}
33+
34+
protected override void OnModelCreating(ModelBuilder modelBuilder)
35+
{
36+
_ = modelBuilder.Entity<HiLoEntity>()
37+
.Property(e => e.Id)
38+
.UseHiLo("HiLoSeq");
39+
}
40+
41+
public DbSet<HiLoEntity> Items => Set<HiLoEntity>();
42+
}
43+
44+
[TestMethod]
45+
public void HiLo_InsertedRows_GetSequentialIds()
46+
{
47+
var simulation = new Simulation();
48+
// Bootstrap the sequence + table manually (no EnsureCreated path).
49+
// EF's HiLo default range is 10 per allocation; the simulator allocates
50+
// one ID at a time, so start at 1 and let the provider's client-side
51+
// allocator handle batching.
52+
_ = simulation.ExecuteNonQuery("""
53+
create sequence HiLoSeq as bigint start with 1 increment by 10;
54+
create table Items (
55+
Id int not null primary key,
56+
Name nvarchar(50) not null
57+
)
58+
""");
59+
using var context = new HiLoDbContext(simulation);
60+
_ = context.Items.Add(new HiLoEntity { Name = "Alice" });
61+
_ = context.Items.Add(new HiLoEntity { Name = "Bob" });
62+
_ = context.Items.Add(new HiLoEntity { Name = "Charlie" });
63+
_ = context.SaveChanges();
64+
65+
var rows = context.Items.OrderBy(e => e.Id).Select(e => new { e.Id, e.Name }).ToArray();
66+
Assert.HasCount(3, rows);
67+
// EF's HiLo allocator pulls the first sequence value (1), then assigns
68+
// Ids 1..N from a client-side range until N hits the increment-by
69+
// (10). The exact IDs depend on EF's internal allocator — assert
70+
// monotonicity and unique-positive instead of pinning values.
71+
Assert.IsGreaterThan(0, rows[0].Id);
72+
Assert.IsGreaterThan(rows[0].Id, rows[1].Id);
73+
Assert.IsGreaterThan(rows[1].Id, rows[2].Id);
74+
Assert.AreEqual("Alice", rows.First(r => r.Id == rows.Min(x => x.Id)).Name);
75+
}
76+
}

0 commit comments

Comments
 (0)