Skip to content

Commit c291c6c

Browse files
committed
Added support for "uniqueidentifier" and related features.
1 parent a003d9c commit c291c6c

14 files changed

Lines changed: 710 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ Heavy-hitters someone might assume work but don't. Source and `git log` are the
7272
- Transactions / locks / MVCC.
7373
- Row-overflow / LOB pages and the `varchar(MAX)` / `nvarchar(MAX)` / `varbinary(MAX)` types they enable.
7474
- Decimal / numeric / float / real / money types. (Also blocks fractional-day `cast(0.5 as datetime)`.)
75-
- `uniqueidentifier`, fixed-length `char(N)` / `nchar(N)` / `binary(N)`, and identity columns.
75+
- Fixed-length `char(N)` / `nchar(N)` / `binary(N)`, and identity columns.
76+
- `NEWSEQUENTIALID()` (deferred until `DEFAULT`-clause support exists in `CREATE TABLE`; the function is only valid in that context).
7677
- Pattern matching (`LIKE`) and `CONVERT`.
7778
- Cross-category `Promote` for `varchar``nvarchar` and integer ↔ string. Only CAST works for those pairs.
7879
- EF Core compatibility: the SqlServer provider downcasts `DbParameter` to `SqlParameter` for some mappings — `DateTime → date`, `DateTime → smalldatetime`, `DateOnly`, `TimeOnly`, `TimeSpan` all break at SaveChanges. See `SimulatedDbParameter` for the matrix; a `SqlServerSimulator.EFCore` adapter package is planned to close the gap.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
namespace SqlServerSimulator;
2+
3+
/// <summary>
4+
/// Exercises the simulator's <c>uniqueidentifier</c> column support through
5+
/// EF Core's idiomatic surface — Guid round-trips, nullable handling, WHERE
6+
/// filtering by Guid, and ordering. EF maps <see cref="Guid"/> to
7+
/// <c>uniqueidentifier</c> with <c>SqlDbType.UniqueIdentifier</c> as the
8+
/// default, so unlike the <c>DateOnly</c> / <c>TimeOnly</c> mappings, this
9+
/// path doesn't trigger the <c>SqlParameter</c>-downcast incompatibility
10+
/// noted on <see cref="SimulatedDbParameter"/>.
11+
/// </summary>
12+
[TestClass]
13+
public class EFCoreUniqueIdentifier
14+
{
15+
public TestContext TestContext { get; set; } = null!;
16+
17+
[TestMethod]
18+
public void Insert_Guid_RoundTrips()
19+
{
20+
using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
21+
22+
var key = Guid.Parse("aabbccdd-eeff-0011-2233-445566778899");
23+
_ = context.Documents.Add(new Document { Id = 1, ExternalKey = key });
24+
_ = context.SaveChanges();
25+
26+
Assert.AreEqual(key, context.Documents.Select(d => d.ExternalKey).First());
27+
}
28+
29+
[TestMethod]
30+
public async Task InsertAsync_Guid_RoundTrips()
31+
{
32+
await using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
33+
34+
var key = Guid.NewGuid();
35+
_ = context.Documents.Add(new Document { Id = 1, ExternalKey = key });
36+
_ = await context.SaveChangesAsync(this.TestContext.CancellationToken);
37+
38+
Assert.AreEqual(key, context.Documents.Select(d => d.ExternalKey).First());
39+
}
40+
41+
[TestMethod]
42+
public void Insert_NullableGuid_AcceptsNull()
43+
{
44+
using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
45+
_ = context.Documents.Add(new Document { Id = 1, ExternalKey = Guid.NewGuid() });
46+
_ = context.SaveChanges();
47+
48+
Assert.IsNull(context.Documents.Select(d => d.OptionalKey).First());
49+
}
50+
51+
[TestMethod]
52+
public void Insert_NullableGuid_AcceptsValue()
53+
{
54+
using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
55+
var optional = Guid.Parse("00000000-0000-0000-0000-000000000001");
56+
_ = context.Documents.Add(new Document { Id = 1, ExternalKey = Guid.NewGuid(), OptionalKey = optional });
57+
_ = context.SaveChanges();
58+
59+
Assert.AreEqual(optional, context.Documents.Select(d => d.OptionalKey).First());
60+
}
61+
62+
[TestMethod]
63+
public void Where_FiltersByGuidEquality()
64+
{
65+
using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
66+
67+
var target = Guid.Parse("aabbccdd-eeff-0011-2233-445566778899");
68+
context.Documents.AddRange(
69+
new Document { Id = 1, ExternalKey = Guid.Parse("11111111-1111-1111-1111-111111111111") },
70+
new Document { Id = 2, ExternalKey = target },
71+
new Document { Id = 3, ExternalKey = Guid.Parse("22222222-2222-2222-2222-222222222222") });
72+
_ = context.SaveChanges();
73+
74+
var match = context.Documents.Where(d => d.ExternalKey == target).Select(d => d.Id).Single();
75+
Assert.AreEqual(2, match);
76+
}
77+
78+
[TestMethod]
79+
public void OrderBy_Guid_UsesSqlServerSortOrder()
80+
{
81+
// The Where filter on parameterized Guid above goes through EF's
82+
// SqlParameter machinery (no string promotion). This test pins the
83+
// server-side ORDER BY behavior — bytes 10..15 most significant,
84+
// matching real SQL Server's quirky uniqueidentifier sort.
85+
using var context = new TestDbContext(TestDbContext.CreateDocumentsSimulation());
86+
87+
Guid[] ids =
88+
[
89+
Guid.Parse("00000000-0000-0000-0000-000000000001"),
90+
Guid.Parse("00000000-0000-0000-0000-010000000000"),
91+
Guid.Parse("00000001-0000-0000-0000-000000000000"),
92+
];
93+
for (var i = 0; i < ids.Length; i++)
94+
_ = context.Documents.Add(new Document { Id = i + 1, ExternalKey = ids[i] });
95+
_ = context.SaveChanges();
96+
97+
var ordered = context.Documents.OrderBy(d => d.ExternalKey).Select(d => d.ExternalKey).ToArray();
98+
99+
// Expected per SQL Server's uid sort: byte 10..15 dominates, so
100+
// 00000001-... (byte 0 = 0x01, all top bytes 0) ranks before
101+
// 00000000-...-000000000001 (byte 15 = 0x01) which ranks before
102+
// 00000000-...-010000000000 (byte 10 = 0x01).
103+
CollectionAssert.AreEqual(
104+
new[]
105+
{
106+
Guid.Parse("00000001-0000-0000-0000-000000000000"),
107+
Guid.Parse("00000000-0000-0000-0000-000000000001"),
108+
Guid.Parse("00000000-0000-0000-0000-010000000000"),
109+
},
110+
ordered);
111+
}
112+
}

SqlServerSimulator.Tests.EFCore/TestDbContext.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ internal class Event
6767

6868
}
6969

70+
/// <summary>
71+
/// Exercises the simulator's <c>uniqueidentifier</c> column support through
72+
/// EF Core. <see cref="ExternalKey"/> is the natural-key identifier (a
73+
/// freshly minted <see cref="Guid"/>); <see cref="OptionalKey"/> covers the
74+
/// nullable column path. Pinning <see cref="Id"/> on a separate
75+
/// <see cref="int"/> primary key keeps EF's tracker out of the
76+
/// uniqueidentifier-comparison story so the tests focus on the type itself.
77+
/// </summary>
78+
internal class Document
79+
{
80+
public int Id { get; set; }
81+
82+
[Column(TypeName = "uniqueidentifier")]
83+
public Guid ExternalKey { get; set; }
84+
85+
[Column(TypeName = "uniqueidentifier")]
86+
public Guid? OptionalKey { get; set; }
87+
}
88+
7089
internal class TestDbContext(Simulation simulation) : DbContext
7190
{
7291
public Simulation Simulation { get; set; } = simulation;
@@ -87,6 +106,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
87106

88107
public DbSet<Event> Events => Set<Event>();
89108

109+
public DbSet<Document> Documents => Set<Document>();
110+
90111
public static Simulation CreateDefaultSimulation(params ReadOnlySpan<int> values)
91112
{
92113
var simulation = new Simulation();
@@ -126,6 +147,22 @@ Avatar varbinary(64) null
126147
return simulation;
127148
}
128149

150+
public static Simulation CreateDocumentsSimulation()
151+
{
152+
var simulation = new Simulation();
153+
_ = simulation
154+
.CreateOpenConnection()
155+
.CreateCommand("""
156+
create table Documents (
157+
Id int,
158+
ExternalKey uniqueidentifier not null,
159+
OptionalKey uniqueidentifier null
160+
)
161+
""")
162+
.ExecuteNonQuery();
163+
return simulation;
164+
}
165+
129166
public static Simulation CreateEventsSimulation()
130167
{
131168
var simulation = new Simulation();

0 commit comments

Comments
 (0)