Skip to content

Commit 8857657

Browse files
committed
Support the "datetime" type.
1 parent 3990c7a commit 8857657

14 files changed

Lines changed: 1594 additions & 789 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,9 @@ Live state is best read from the code itself and recent commit history. Architec
7171
- Transactions / locks / MVCC: not implemented. The page foundation is in place; the next phase hasn't started.
7272
- Row-overflow / LOB pages: not modeled. Rows whose encoded size exceeds SQL Server's 8060-byte in-row limit throw at insert time; `varchar(MAX)` is parsed but rejected with a clear "not yet supported" error rather than silently fall through.
7373
- Heap allocation: flat page list. No IAM/PFS allocation tracking.
74-
- Cross-string-type coercion (`varchar``nvarchar`): not modeled. The binary-expression `Promote` path's cross-category handling is also not modeled (string ↔ integer in expressions still errors). String → integer-family `CAST` is implemented, with the SQL-Server-matched Msg 244 / 245 / 248 / 8115 error variants.
74+
- Cross-string-type coercion (`varchar``nvarchar`) and other cross-category pairs (string ↔ integer in binary expressions): the binary-expression `Promote` path rejects them. String → integer-family `CAST` is implemented, with the SQL-Server-matched Msg 244 / 245 / 248 / 8115 error variants. Cross-family date/time pairs in `Promote` are wired (highest-precedence-with-max-precision wins; `time` paired with non-time raises Msg 402).
75+
- Numeric ↔ `datetime` coercion (`cast(0 as datetime)`, `dt + 1`): not modeled. SQL Server treats integers as days-since-1900-01-01; the simulator throws `NotSupportedException` for the pair.
76+
- `smalldatetime`: not yet modeled. The simulator's `GetByName` rejects the type name; this is the only remaining standard date/time type EF Core may emit.
77+
- `ORDER BY`: not modeled. The trailing clause is consumed but not honored, so rows come back in insertion order. EF Core queries that rely on ordering need an in-memory `.OrderBy(...)` after materialization.
7578
- Pattern matching (`LIKE`), `CONVERT`, fixed-length `char(N)` / `nchar(N)` / `binary(N)`, and identity columns: not modeled.
79+
- CAST to a smaller varchar/nvarchar than the value renders: SQL Server silently truncates (e.g. `cast(dt as varchar(10))` → first 10 chars). The simulator returns the full string today; truncation isn't enforced on the CAST path.

SqlServerSimulator.Tests.EFCore/EFCoreDateTime.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,74 @@ public void Where_FiltersByDateTimeOffsetEquality_CrossOffset()
172172
var match = context.Events.Where(e => e.OccurredAt == west).Select(e => e.Id).Single();
173173
Assert.AreEqual(1, match);
174174
}
175+
176+
[TestMethod]
177+
public void Insert_LegacyDateTime_RoundTripsAtTickGranularity()
178+
{
179+
// Legacy datetime stores 1/300-second ticks. .997 input lands on
180+
// tick 299 — preserved by EF Core's reader hydration since SqlClient
181+
// reconstructs DateTime ticks deterministically from the stored unit.
182+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
183+
184+
var started = new DateTime(2026, 5, 4, 13, 45, 30, 997);
185+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Started = started });
186+
_ = context.SaveChanges();
187+
188+
// Round-trip preserves the tick-quantized value, not the raw ms.
189+
var read = context.Events.Select(e => e.Started).First();
190+
Assert.IsNotNull(read);
191+
Assert.AreEqual(started.Date, read.Value.Date);
192+
Assert.AreEqual(started.Hour, read.Value.Hour);
193+
Assert.AreEqual(started.Minute, read.Value.Minute);
194+
Assert.AreEqual(started.Second, read.Value.Second);
195+
// .997 ms input → tick 299 → stored at 9_966_666 100-ns ticks past the second.
196+
Assert.AreEqual(9_966_666, read.Value.Ticks % TimeSpan.TicksPerSecond);
197+
}
198+
199+
[TestMethod]
200+
public void Insert_LegacyDateTime_999msRollsToNextSecond()
201+
{
202+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
203+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Started = new DateTime(2026, 5, 4, 13, 45, 30, 999) });
204+
_ = context.SaveChanges();
205+
206+
Assert.AreEqual(new DateTime(2026, 5, 4, 13, 45, 31), context.Events.Select(e => e.Started).First());
207+
}
208+
209+
[TestMethod]
210+
public void Insert_NullableLegacyDateTime_AcceptsNull()
211+
{
212+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
213+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4) });
214+
_ = context.SaveChanges();
215+
216+
Assert.IsNull(context.Events.Select(e => e.Started).First());
217+
Assert.IsNull(context.Events.Select(e => e.Ended).First());
218+
}
219+
220+
[TestMethod]
221+
public void Where_FiltersByLegacyDateTimeEquality()
222+
{
223+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
224+
var target = new DateTime(2026, 5, 4, 13, 45, 30);
225+
context.Events.AddRange(
226+
new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Started = new DateTime(2026, 5, 4, 12, 0, 0) },
227+
new Event { Id = 2, CreatedAt = new DateTime(2026, 5, 4), Started = target },
228+
new Event { Id = 3, CreatedAt = new DateTime(2026, 5, 4), Started = new DateTime(2026, 5, 4, 15, 0, 0) });
229+
_ = context.SaveChanges();
230+
231+
var match = context.Events.Where(e => e.Started == target).Select(e => e.Id).Single();
232+
Assert.AreEqual(2, match);
233+
}
234+
235+
[TestMethod]
236+
public void Insert_LegacyDateTime_AtMin_RoundTrips()
237+
{
238+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
239+
var min = new DateTime(1753, 1, 1);
240+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Started = min });
241+
_ = context.SaveChanges();
242+
243+
Assert.AreEqual(min, context.Events.Select(e => e.Started).First());
244+
}
175245
}

SqlServerSimulator.Tests.EFCore/TestDbContext.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,13 @@ internal class Person
2929
}
3030

3131
/// <summary>
32-
/// Exercises the simulator's <c>datetime2</c> and <c>datetimeoffset</c>
33-
/// column support through EF Core. The CLR-side properties stay narrow:
34-
/// <see cref="DateTime"/> for datetime2 and <see cref="DateTimeOffset"/>
35-
/// for datetimeoffset, because EF Core's SqlServer provider downcasts
36-
/// <see cref="System.Data.Common.DbParameter"/> to <c>SqlParameter</c> in
37-
/// the mappings for <see cref="DateOnly"/>, <see cref="TimeOnly"/>,
38-
/// <see cref="TimeSpan"/>, and <c>DateTime → date</c>. The two configurations
39-
/// covered here are the ones that leave the default <c>SqlDbType</c> in
40-
/// place and so don't trigger the downcast. See
32+
/// Exercises the simulator's <c>datetime</c>, <c>datetime2</c>, and
33+
/// <c>datetimeoffset</c> column support through EF Core. CLR-side properties
34+
/// are <see cref="DateTime"/> and <see cref="DateTimeOffset"/> only;
35+
/// <see cref="DateOnly"/>, <see cref="TimeOnly"/>, <see cref="TimeSpan"/>,
36+
/// and <c>DateTime → date</c> remain unreachable through EF Core's SqlServer
37+
/// provider because those mappings downcast
38+
/// <see cref="System.Data.Common.DbParameter"/> to <c>SqlParameter</c>. See
4139
/// <see cref="SimulatedDbParameter"/> for the full compatibility matrix.
4240
/// </summary>
4341
internal class Event
@@ -55,6 +53,17 @@ internal class Event
5553

5654
[Column(TypeName = "datetimeoffset(3)")]
5755
public DateTimeOffset? Cancelled { get; set; }
56+
57+
/// <remarks>
58+
/// Nullable to keep <see cref="DateTime"/>'s default of <c>0001-01-01</c>
59+
/// from blowing up legacy <c>datetime</c>'s 1753-9999 range when the
60+
/// existing event tests omit it.
61+
/// </remarks>
62+
[Column(TypeName = "datetime")]
63+
public DateTime? Started { get; set; }
64+
65+
[Column(TypeName = "datetime")]
66+
public DateTime? Ended { get; set; }
5867
}
5968

6069
internal class TestDbContext(Simulation simulation) : DbContext
@@ -104,7 +113,14 @@ public static Simulation CreatePeopleSimulation()
104113
var simulation = new Simulation();
105114
_ = simulation
106115
.CreateOpenConnection()
107-
.CreateCommand("create table People ( Id int, Name nvarchar(50) not null, Code varchar(10) null, Avatar varbinary(64) null )")
116+
.CreateCommand("""
117+
create table People (
118+
Id int,
119+
Name nvarchar(50) not null,
120+
Code varchar(10) null,
121+
Avatar varbinary(64) null
122+
)
123+
""")
108124
.ExecuteNonQuery();
109125
return simulation;
110126
}
@@ -114,7 +130,17 @@ public static Simulation CreateEventsSimulation()
114130
var simulation = new Simulation();
115131
_ = simulation
116132
.CreateOpenConnection()
117-
.CreateCommand("create table Events ( Id int, CreatedAt datetime2(7) not null, Updated datetime2(3) null, OccurredAt datetimeoffset(7) not null, Cancelled datetimeoffset(3) null )")
133+
.CreateCommand("""
134+
create table Events (
135+
Id int,
136+
CreatedAt datetime2(7) not null,
137+
Updated datetime2(3) null,
138+
OccurredAt datetimeoffset(7) not null,
139+
Cancelled datetimeoffset(3) null,
140+
Started datetime null,
141+
Ended datetime null
142+
)
143+
""")
118144
.ExecuteNonQuery();
119145
return simulation;
120146
}

0 commit comments

Comments
 (0)