Skip to content

Commit 76c20f0

Browse files
committed
BACPAC fidelity polish — natively-compiled procedures + geography WKB decoder. Two related bundles that close the last two known gaps across the gathered samples: WideWorldImporters-Full reaches **zero Skipped categories** (every element type loads end-to-end), and AdventureWorks2025's Person.Address.SpatialLocation column decodes to POINT (long lat) WKT for all 19,614 rows instead of dropping to NULL. Both flagship public bacpacs are now structurally + data-complete in the simulator.
# Bundle 1 — Natively-compiled SP body WWI-Full's `Website.RecordColdRoomTemperatures` is the only natively-compiled SP across the gathered samples. The DACFx-emitted CREATE shape is `CREATE PROCEDURE … WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER AS BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English') <body> END`. Two parser changes unblock it: **`Simulation/Simulation.CreateProcedure.cs::ParseProcedureWithOptions`** now accepts `SCHEMABINDING` and `NATIVE_COMPILATION` alongside the previously-handled RECOMPILE / EXECUTE AS / ENCRYPTION / FOR REPLICATION. Both new options are parse-and-discard — the simulator doesn't model schema-binding enforcement or native compilation. `Native_Compilation` joins `ContextualKeyword`; `SchemaBinding` was already there. **`Simulation/Simulation.cs`** BEGIN ATOMIC dispatch arm flipped from `throw NotSupportedException` to call into a new `ParseBeginAtomicBlock` in `Simulation/Simulation.IfBlock.cs`. The new method consumes `BEGIN ATOMIC [WITH (...)]`, balanced-paren-skips the WITH options (TRANSACTION ISOLATION LEVEL / LANGUAGE / DATEFORMAT / DATEFIRST / DELAYED_DURABILITY — no semantic effect in the simulator), then dispatches body statements like `ParseBeginBlock` until `END`. The atomic-transaction boundary that real SQL Server enforces (the block is its own transaction) is approximated by the existing implicit per-statement undo plus any outer explicit transaction — sufficient for SqlPackage-emitted bodies and for any production code that doesn't rely on per-block isolation overrides, but a caller asserting "this block opened a new tx" would see fall-through to the session's isolation level. **Tests** in `SqlServerSimulator.Tests/StoredProcedureTests.cs`: - `Create_With_NativeCompilation_Schemabinding_Parses` — header-only smoke (NATIVE_COMPILATION + SCHEMABINDING + EXECUTE AS OWNER on a SELECT-1 body). - `BeginAtomic_Body_Runs` — INSERT inside the atomic body lands rows when the proc is EXEC'd. - `BeginAtomic_With_TryCatch_Body` — mirrors the WWI-Full SP shape (BEGIN ATOMIC wrapping BEGIN TRY / BEGIN CATCH with THROW); verifies CATCH-side INSERT runs. - `BeginAtomic_Without_With_Options_Block_Parses` — bare BEGIN ATOMIC (no WITH options) — future-compat for ATOMIC outside natively-compiled procs. - `BeginAtomic_Empty_Body_RaisesSyntax_On_Exec` — empty atomic body rejected at EXEC time (matches BEGIN…END empty-body rule). The prior `IfBlockTests.BeginAtomic_NotSupported` test was inverted to `BeginAtomic_AtBatchTopLevel_DispatchesBody` since BEGIN ATOMIC at batch top level now succeeds. WWI-Full Skipped: 1 → 0. The `Load_WWIFull_Known_Gaps_Recorded_In_Skipped` regression test now asserts `IsEmpty(diagnostics.Skipped)`. # Bundle 2 — Geography simple-point WKB → WKT decoder AW's 19,614 `Person.Address.SpatialLocation` rows previously loaded as NULL — the BCP wire format was drained (so subsequent column boundaries didn't corrupt) but the bytes weren't decoded. AW uses only simple points, so the simple-point shortcut path covers the full sample. **New `Storage/Bacpac/SpatialWkbDecoder.cs`**: `TryDecodeSimplePoint(wkb, isGeography)` decodes the Microsoft spatial UDT simple-point shape: - 4-byte SRID (LE int32) - 1-byte version (0x01 or 0x02 accepted) - 1-byte properties bitfield — `IsSinglePoint` (0x08) required; `HasZ` / `HasM` reject (simple-point shortcut is 2D only) - 16-byte coordinate payload (two IEEE 754 doubles, LE) Total wire length is exactly 22 bytes — any other length returns null. **Geography axis inversion**: geography stores `(lat, long)` in binary but WKT prints `POINT (long lat)` per OGC; geometry uses `(x, y)` for both. WKT formatting uses `CultureInfo.InvariantCulture` with the `"R"` round-trip specifier so coordinate values reproduce bit-for-bit on re-parse. Complex shapes (LineString, Polygon, MultiPolygon, FullGlobe, GeometryCollection, anything with Z/M coordinates, unknown versions) return null and the row loader falls back to `SqlValue.Null` — keeps the loader resilient so a single non-simple-point row in a future sample doesn't fail its whole BCP file. **`Storage/Bacpac/BcpRowReader.cs`** split the prior `SpatialDeferToNull` payload kind into separate `Geography` + `Geometry` payloads so each calls the decoder with the right axis-order flag. The decoder result is wrapped via `SqlValue.FromGeography` / `SqlValue.FromGeometry` on success; null result falls back to `SqlValue.Null(type)`. **Tests** in `SqlServerSimulator.Tests.Internal/Storage/SpatialWkbDecoderTests.cs` (7 new): axis inversion (geography vs geometry), bit-for-bit coordinate round-trip via the `"R"` specifier, truncated payload, non-simple-point properties bit, Z/M bits with IsSinglePoint set, unknown version. The previous `Load_AW_Geography_Column_Drops_To_Null` regression test in `BacpacLoaderTests` was inverted to `Load_AW_Geography_Column_Decodes_To_Point_Wkt` — asserts 0 NULL rows, all 19,614 round-trip, and spot-checks that AddressID=1's SpatialLocation casts to a `POINT (` WKT string. # Cross-cutting doc updates - `CLAUDE.md`: "What's modeled" programmable-objects pointer gains the NATIVE_COMPILATION / BEGIN ATOMIC paragraph (with the BEGIN-ATOMIC-is-not-a-real-tx-boundary caveat); BACPAC pointer entry's WWI-Full status now reads "zero remaining `Skipped` categories"; spatial paragraph updated from "WKB→WKT decoding deferred" to the simple-point shortcut summary. - `docs/claude/bacpac-prerequisites.md` step 13 (natively-compiled SP) struck through with the full implementation detail; step 14 (geography WKB decoder) struck through likewise; Status section updated.
1 parent bd022ad commit 76c20f0

14 files changed

Lines changed: 441 additions & 38 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

SqlServerSimulator.Tests.Internal/Storage/BacpacLoaderTests.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,29 +382,30 @@ public void Load_AW_VarbinaryMax_Column_Carries_Bytes()
382382
}
383383

384384
[TestMethod]
385-
public void Load_AW_Geography_Column_Drops_To_Null()
385+
public void Load_AW_Geography_Column_Decodes_To_Point_Wkt()
386386
{
387387
var simulation = LoadAdventureWorks(out _);
388388
using var connection = (SimulatedDbConnection)simulation.CreateDbConnection();
389389
connection.Open();
390390

391-
// Person.Address rows load (geography wire format decoded) but
392-
// SpatialLocation column stores NULL — WKB-to-WKT translation is
393-
// deferred so the bytes are read-and-discarded. The row count
394-
// proves the wire-format reader didn't corrupt subsequent column
395-
// boundaries (rowguid + ModifiedDate after SpatialLocation must
396-
// round-trip cleanly).
391+
// After the simple-point WKB decoder bundle (2026-05-16), every AW
392+
// SpatialLocation row decodes to a `POINT (long lat)` WKT string.
393+
// AW's geography values are all simple points (no LineString /
394+
// Polygon / etc.), so 19,614 / 19,614 round-trip.
397395
AreEqual(19614, QueryCount(connection, "SELECT COUNT(*) FROM Person.Address;"));
398-
AreEqual(19614, QueryCount(connection, "SELECT COUNT(*) FROM Person.Address WHERE SpatialLocation IS NULL;"));
396+
AreEqual(0, QueryCount(connection, "SELECT COUNT(*) FROM Person.Address WHERE SpatialLocation IS NULL;"));
399397

400-
// Spot-check that following columns survived the geography read.
398+
// Spot-check that the WKT round-trips through CAST AS nvarchar(max).
399+
// AddressID = 1 is a Bothell, WA address; longitude ~-122, latitude ~47.
401400
using var command = connection.CreateCommand();
402-
command.CommandText = "SELECT TOP(1) AddressID, City, PostalCode FROM Person.Address WHERE AddressID = 1;";
401+
command.CommandText = "SELECT TOP(1) AddressID, City, PostalCode, CAST(SpatialLocation AS nvarchar(max)) FROM Person.Address WHERE AddressID = 1;";
403402
using var reader = command.ExecuteReader();
404403
IsTrue(reader.Read());
405404
AreEqual(1, reader.GetInt32(0));
406405
AreEqual("Bothell", reader.GetString(1));
407406
AreEqual("98011", reader.GetString(2));
407+
var wkt = reader.GetString(3);
408+
IsTrue(wkt.StartsWith("POINT (", StringComparison.Ordinal), $"expected WKT to start with 'POINT (' but got '{wkt}'");
408409
}
409410

410411
[TestMethod]
@@ -967,13 +968,12 @@ public void Load_WWIFull_Known_Gaps_Recorded_In_Skipped()
967968
IsFalse(grouped.ContainsKey("SqlPartitionFunction"), "Partition functions are filegroup-mapping metadata; skip-with-diagnostic.");
968969
IsFalse(grouped.ContainsKey("SqlPartitionScheme"), "Partition schemes are filegroup-mapping metadata; skip-with-diagnostic.");
969970

970-
// NATIVE_COMPILATION procedure body is the sole remaining loader
971-
// gap for WWI-Full (1 procedure: Website.RecordColdRoomTemperatures).
972-
// The WITH NATIVE_COMPILATION = ON ... BEGIN ATOMIC body decoration
973-
// isn't parsed; the procedure body falls onto Skipped with a
974-
// SimulatedSqlException parse failure.
975-
IsTrue(grouped.TryGetValue("SqlProcedure", out var procSkipped));
976-
AreEqual(1, procSkipped);
971+
// NATIVE_COMPILATION + BEGIN ATOMIC body parsers shipped 2026-05-16:
972+
// Website.RecordColdRoomTemperatures (the sole natively-compiled SP
973+
// in WWI-Full) loads. Combined with the temporal-table wire-up
974+
// landing the same day, WWI-Full reaches zero Skipped categories.
975+
IsFalse(grouped.ContainsKey("SqlProcedure"), "NATIVE_COMPILATION + BEGIN ATOMIC body parsers landed; the natively-compiled SP loads.");
976+
IsEmpty(diagnostics.Skipped);
977977
}
978978

979979
[TestMethod]
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Buffers.Binary;
2+
using SqlServerSimulator.Storage.Bacpac;
3+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
4+
5+
namespace SqlServerSimulator.Storage;
6+
7+
/// <summary>
8+
/// Decoder fidelity tests for <see cref="SpatialWkbDecoder"/>. Covers the
9+
/// simple-point case (the dominant shape in AdventureWorks
10+
/// <c>Person.Address.SpatialLocation</c>) plus the fall-through paths for
11+
/// shapes the decoder doesn't model yet.
12+
/// </summary>
13+
[TestClass]
14+
public sealed class SpatialWkbDecoderTests
15+
{
16+
[TestMethod]
17+
public void SimplePoint_Geography_AxisInversion()
18+
{
19+
// Geography stores (lat, long) but WKT emits (long, lat). Build a
20+
// simple-point payload for lat=47.6, long=-122.3 and verify WKT
21+
// prints long first.
22+
var wkb = BuildSimplePoint(srid: 4326, firstCoord: 47.6, secondCoord: -122.3);
23+
var wkt = SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: true);
24+
AreEqual("POINT (-122.3 47.6)", wkt);
25+
}
26+
27+
[TestMethod]
28+
public void SimplePoint_Geometry_NoAxisInversion()
29+
{
30+
// Geometry stores and prints (x, y) in the same order.
31+
var wkb = BuildSimplePoint(srid: 0, firstCoord: 1.5, secondCoord: 2.5);
32+
var wkt = SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: false);
33+
AreEqual("POINT (1.5 2.5)", wkt);
34+
}
35+
36+
[TestMethod]
37+
public void SimplePoint_RoundTripsBitForBit()
38+
{
39+
// The "R" round-trip specifier preserves the full precision of an
40+
// IEEE 754 double through ToString. A re-parsed WKT must produce
41+
// bit-identical coordinates.
42+
var original = -122.13469409942627; // sample geography long from AW
43+
var wkb = BuildSimplePoint(srid: 4326, firstCoord: 47.642438, secondCoord: original);
44+
var wkt = SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: true);
45+
IsNotNull(wkt);
46+
// Parse the first coordinate back from WKT and verify it round-trips.
47+
var openParen = wkt.IndexOf('(');
48+
var space = wkt.IndexOf(' ', openParen);
49+
var firstStr = wkt[(openParen + 1)..space];
50+
AreEqual(original, double.Parse(firstStr, System.Globalization.CultureInfo.InvariantCulture));
51+
}
52+
53+
[TestMethod]
54+
public void Truncated_Payload_ReturnsNull()
55+
{
56+
// Anything other than exactly 22 bytes can't be a simple-point —
57+
// decoder returns null so the row loader falls back to SqlValue.Null.
58+
var truncated = new byte[10];
59+
IsNull(SpatialWkbDecoder.TryDecodeSimplePoint(truncated, isGeography: true));
60+
}
61+
62+
[TestMethod]
63+
public void NonSimplePoint_Properties_ReturnsNull()
64+
{
65+
// Properties byte without IsSinglePoint (0x08) bit means LineString /
66+
// Polygon / etc. — decoder returns null for the fall-back.
67+
var wkb = new byte[22];
68+
BinaryPrimitives.WriteInt32LittleEndian(wkb.AsSpan(0, 4), 4326);
69+
wkb[4] = 0x01; // version
70+
wkb[5] = 0x00; // properties — no IsSinglePoint
71+
IsNull(SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: true));
72+
}
73+
74+
[TestMethod]
75+
public void ZorM_Bits_ReturnsNull()
76+
{
77+
// Z or M coordinates would extend the payload past 22 bytes, but
78+
// even if the IsSinglePoint bit is set the decoder bails because
79+
// the simple-point shortcut doesn't support 3D / measured points.
80+
var wkb = BuildSimplePoint(srid: 4326, firstCoord: 0, secondCoord: 0);
81+
wkb[5] = 0x08 | 0x01; // IsSinglePoint + HasZ
82+
IsNull(SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: true));
83+
}
84+
85+
[TestMethod]
86+
public void UnknownVersion_ReturnsNull()
87+
{
88+
var wkb = BuildSimplePoint(srid: 4326, firstCoord: 0, secondCoord: 0);
89+
wkb[4] = 0xFF; // unknown version
90+
IsNull(SpatialWkbDecoder.TryDecodeSimplePoint(wkb, isGeography: true));
91+
}
92+
93+
/// <summary>
94+
/// Builds a Microsoft-spatial-binary simple-point payload: 4-byte SRID,
95+
/// 1-byte version, 1-byte properties (IsSinglePoint), 8-byte first coord,
96+
/// 8-byte second coord. Geography callers pass (lat, long); geometry
97+
/// callers pass (x, y).
98+
/// </summary>
99+
private static byte[] BuildSimplePoint(int srid, double firstCoord, double secondCoord)
100+
{
101+
var buf = new byte[22];
102+
BinaryPrimitives.WriteInt32LittleEndian(buf.AsSpan(0, 4), srid);
103+
buf[4] = 0x01; // version
104+
buf[5] = 0x08; // IsSinglePoint
105+
BinaryPrimitives.WriteDoubleLittleEndian(buf.AsSpan(6, 8), firstCoord);
106+
BinaryPrimitives.WriteDoubleLittleEndian(buf.AsSpan(14, 8), secondCoord);
107+
return buf;
108+
}
109+
}

SqlServerSimulator.Tests/IfBlockTests.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,17 @@ public void IfBody_NestedBlock_DispatchesInnerStatement()
172172
"begin if 1=1 select 'inside-if' end"));
173173

174174
[TestMethod]
175-
public void BeginAtomic_NotSupported()
176-
=> Throws<NotSupportedException>(() => new Simulation().ExecuteNonQuery(
175+
public void BeginAtomic_AtBatchTopLevel_DispatchesBody()
176+
{
177+
// BEGIN ATOMIC at batch top level (no enclosing CREATE PROCEDURE)
178+
// is uncommon but legal grammar. The body dispatches like a regular
179+
// BEGIN…END block; the WITH (...) options block parses-and-discards.
180+
// Coverage of the natively-compiled-SP shape lives in
181+
// StoredProcedureTests; this one verifies the dispatcher path
182+
// outside the procedure context.
183+
AreEqual(1, new Simulation().ExecuteScalar(
177184
"begin atomic with (transaction isolation level = snapshot, language = N'us_english') select 1 end"));
185+
}
178186

179187
[TestMethod]
180188
public void BeginDistributedTran_NotSupported()

SqlServerSimulator.Tests/StoredProcedureTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,118 @@ public void ObjectId_With_Type_P_Resolves_Procedures()
428428
IsGreaterThan(0, IsInstanceOfType<int>(id));
429429
}
430430

431+
// -- WITH NATIVE_COMPILATION / SCHEMABINDING + BEGIN ATOMIC body --
432+
// Tests cover the natively-compiled procedure shape SqlPackage emits in
433+
// bacpacs (WWI-Full's Website.RecordColdRoomTemperatures is the canonical
434+
// example). The simulator's semantic model doesn't change: NATIVE_COMPILATION
435+
// is parse-and-discard, BEGIN ATOMIC is parsed as a regular block whose
436+
// WITH (...) options block is consumed without enforcement.
437+
438+
[TestMethod]
439+
public void Create_With_NativeCompilation_Schemabinding_Parses()
440+
{
441+
var sim = new Simulation();
442+
_ = sim.ExecuteNonQuery("""
443+
create procedure dbo.p
444+
with native_compilation, schemabinding, execute as owner
445+
as
446+
begin atomic with (transaction isolation level = snapshot, language = N'English')
447+
select 1;
448+
end
449+
""");
450+
AreEqual(1, sim.ExecuteScalar("select count(*) from sys.procedures where name = 'p'"));
451+
}
452+
453+
[TestMethod]
454+
public void BeginAtomic_Body_Runs()
455+
{
456+
// Verify the BEGIN ATOMIC body actually dispatches: a procedure with
457+
// a body that inserts into a table should land the row when EXEC'd.
458+
var sim = new Simulation();
459+
sim.ExecuteBatches(
460+
"create table t (id int primary key, v int)",
461+
"""
462+
create procedure dbo.add_row @id int, @v int
463+
with native_compilation, schemabinding, execute as owner
464+
as
465+
begin atomic with (transaction isolation level = snapshot, language = N'us_english')
466+
insert into t (id, v) values (@id, @v);
467+
end
468+
""",
469+
"exec dbo.add_row 1, 100",
470+
"exec dbo.add_row 2, 200");
471+
AreEqual(2, sim.ExecuteScalar("select count(*) from t"));
472+
AreEqual(200, sim.ExecuteScalar("select v from t where id = 2"));
473+
}
474+
475+
[TestMethod]
476+
public void BeginAtomic_With_TryCatch_Body()
477+
{
478+
// Mirrors WWI-Full's Website.RecordColdRoomTemperatures shape:
479+
// BEGIN ATOMIC WITH (...) wrapping a BEGIN TRY / BEGIN CATCH block.
480+
var sim = new Simulation();
481+
sim.ExecuteBatches(
482+
"create table failures (msg nvarchar(200))",
483+
"""
484+
create procedure dbo.try_or_log @raise bit
485+
with native_compilation, schemabinding, execute as owner
486+
as
487+
begin atomic with (transaction isolation level = snapshot, language = N'English')
488+
begin try
489+
if @raise = 1
490+
throw 51000, N'boom', 1;
491+
end try
492+
begin catch
493+
insert into failures (msg) values (error_message());
494+
end catch
495+
end
496+
""",
497+
"exec dbo.try_or_log 0",
498+
"exec dbo.try_or_log 1");
499+
AreEqual(1, sim.ExecuteScalar("select count(*) from failures"));
500+
AreEqual("boom", sim.ExecuteScalar("select msg from failures"));
501+
}
502+
503+
[TestMethod]
504+
public void BeginAtomic_Without_With_Options_Block_Parses()
505+
{
506+
// The grammar allows BEGIN ATOMIC without a WITH (...) options
507+
// block (future ATOMIC use cases outside natively-compiled procs).
508+
// Verify the path doesn't reject.
509+
var sim = new Simulation();
510+
sim.ExecuteBatches(
511+
"create table t (id int)",
512+
"""
513+
create procedure dbo.p
514+
as
515+
begin atomic
516+
insert into t values (42);
517+
end
518+
""",
519+
"exec dbo.p");
520+
AreEqual(42, sim.ExecuteScalar("select id from t"));
521+
}
522+
523+
[TestMethod]
524+
public void BeginAtomic_Empty_Body_RaisesSyntax_On_Exec()
525+
{
526+
// Empty atomic body should be rejected (matches the regular
527+
// BEGIN…END empty-body rule). The procedure CREATE captures the
528+
// body as opaque text; the rejection fires when EXEC re-parses
529+
// and dispatches the body. ExecuteBatches keeps the CREATE and
530+
// EXEC in separate batches so the body text doesn't accidentally
531+
// include the EXEC.
532+
var sim = new Simulation();
533+
_ = sim.ExecuteNonQuery("""
534+
create procedure dbo.p
535+
as
536+
begin atomic with (transaction isolation level = snapshot, language = N'English')
537+
end
538+
""");
539+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("exec dbo.p"));
540+
AreEqual("102", ex.Data["HelpLink.EvtID"]);
541+
}
542+
431543
private static void StringContains(string actual, string needle)
432544
=> IsTrue(actual.Contains(needle, StringComparison.Ordinal), $"expected '{actual}' to contain '{needle}'");
433545

SqlServerSimulator/Parser/ContextualKeyword.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ enum ContextualKeyword
5454
Log,
5555
Matched,
5656
Max,
57+
Native_Compilation,
5758
MaxRecursion,
5859
MaxValue,
5960
MinValue,
@@ -80,7 +81,7 @@ enum ContextualKeyword
8081
Rollup,
8182
Row,
8283
Rows,
83-
Schemabinding,
84+
SchemaBinding,
8485
Scoped,
8586
Sequence,
8687
Sets,

SqlServerSimulator/Simulation/Simulation.CreateFunction.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private static bool ParseScalarTail(ParserContext context, Schema schema, MultiP
239239
returnsNullOnNullInput = true;
240240
context.MoveNextRequired();
241241
break;
242-
case UnquotedString { ContextualKeyword: ContextualKeyword.Schemabinding }:
242+
case UnquotedString { ContextualKeyword: ContextualKeyword.SchemaBinding }:
243243
case UnquotedString { ContextualKeyword: ContextualKeyword.Encryption }:
244244
context.MoveNextRequired();
245245
break;
@@ -410,7 +410,7 @@ private static void ParseInlineTvfOptions(ParserContext context)
410410
{
411411
switch (context.Token)
412412
{
413-
case UnquotedString { ContextualKeyword: ContextualKeyword.Schemabinding }:
413+
case UnquotedString { ContextualKeyword: ContextualKeyword.SchemaBinding }:
414414
case UnquotedString { ContextualKeyword: ContextualKeyword.Encryption }:
415415
// Parse-and-ignore. Fidelity gap on SCHEMABINDING
416416
// documented in CLAUDE.md.

SqlServerSimulator/Simulation/Simulation.CreateProcedure.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,13 @@ private static void ParseProcedureWithOptions(ParserContext context)
316316
{
317317
case UnquotedString { ContextualKeyword: ContextualKeyword.Recompile }:
318318
case UnquotedString { ContextualKeyword: ContextualKeyword.Encryption }:
319+
case UnquotedString { ContextualKeyword: ContextualKeyword.SchemaBinding }:
320+
case UnquotedString { ContextualKeyword: ContextualKeyword.Native_Compilation }:
321+
// SCHEMABINDING / NATIVE_COMPILATION parse-and-ignore: the
322+
// simulator doesn't model schema-binding enforcement or
323+
// native-compilation code paths. NATIVE_COMPILATION pairs
324+
// with a BEGIN ATOMIC body block (which the dispatcher
325+
// handles by re-using the regular BEGIN…END flow).
319326
context.MoveNextRequired();
320327
break;
321328
case ReservedKeyword { Keyword: Keyword.Execute }:

SqlServerSimulator/Simulation/Simulation.CreateView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ private static void ParseViewOptions(ParserContext context)
176176
while (true)
177177
{
178178
if (context.Token is not UnquotedString opt
179-
|| opt.ContextualKeyword is not (ContextualKeyword.Schemabinding or ContextualKeyword.Encryption or ContextualKeyword.View_Metadata))
179+
|| opt.ContextualKeyword is not (ContextualKeyword.SchemaBinding or ContextualKeyword.Encryption or ContextualKeyword.View_Metadata))
180180
{
181181
throw SimulatedSqlException.SyntaxErrorNear(context);
182182
}

0 commit comments

Comments
 (0)