Skip to content

Commit 14bb1b8

Browse files
committed
Support "time" type. Test and other clean-up.
1 parent 7ba8630 commit 14bb1b8

18 files changed

Lines changed: 603 additions & 99 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
namespace SqlServerSimulator;
2+
3+
/// <summary>
4+
/// Exercises the simulator's <c>datetime2(N)</c> column support through EF
5+
/// Core's idiomatic surface. Confirms the full-precision (precision 7) round
6+
/// trip and the lower-precision (precision 3) rounding-on-store behavior land
7+
/// correctly through EF's parameter binding and reader hydration.
8+
/// </summary>
9+
/// <remarks>
10+
/// Only <see cref="DateTime"/> properties are covered today — see
11+
/// <see cref="SimulatedDbParameter"/> for the per-mapping compatibility table
12+
/// explaining why the other date/time configurations
13+
/// (<c>DateOnly → date</c>, <c>TimeOnly → time</c>, <c>TimeSpan → time</c>,
14+
/// <c>DateTime → date</c>) are unreachable through EF Core until a bridge
15+
/// adapter ships.
16+
/// </remarks>
17+
[TestClass]
18+
public class EFCoreDateTime
19+
{
20+
public TestContext TestContext { get; set; } = null!;
21+
22+
[TestMethod]
23+
public void Insert_DateTime_FullPrecisionRoundTrips()
24+
{
25+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
26+
27+
var dt = new DateTime(2026, 5, 4, 13, 45, 30).AddTicks(1234567);
28+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = dt });
29+
_ = context.SaveChanges();
30+
31+
Assert.AreEqual(dt, context.Events.Select(e => e.CreatedAt).First());
32+
}
33+
34+
[TestMethod]
35+
public async Task InsertAsync_DateTime_RoundTrips()
36+
{
37+
await using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
38+
39+
var dt = new DateTime(2026, 5, 4, 13, 45, 30);
40+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = dt });
41+
_ = await context.SaveChangesAsync(this.TestContext.CancellationToken);
42+
43+
Assert.AreEqual(dt, context.Events.Select(e => e.CreatedAt).First());
44+
}
45+
46+
[TestMethod]
47+
public void Insert_NullableDateTime_AcceptsNull()
48+
{
49+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
50+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4) });
51+
_ = context.SaveChanges();
52+
53+
Assert.IsNull(context.Events.Select(e => e.Updated).First());
54+
}
55+
56+
[TestMethod]
57+
public void Insert_NullableDateTime_AcceptsValue()
58+
{
59+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
60+
var updated = new DateTime(2026, 5, 4, 14, 0, 0, 250);
61+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Updated = updated });
62+
_ = context.SaveChanges();
63+
64+
Assert.AreEqual(updated, context.Events.Select(e => e.Updated).First());
65+
}
66+
67+
[TestMethod]
68+
public void Insert_LowerPrecisionColumn_RoundsHalfUp()
69+
{
70+
// Updated is datetime2(3); 0.5ms above a millisecond boundary rounds to next ms.
71+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
72+
var updated = new DateTime(2026, 5, 4, 13, 45, 30, 100).AddTicks(5_000);
73+
_ = context.Events.Add(new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4), Updated = updated });
74+
_ = context.SaveChanges();
75+
76+
Assert.AreEqual(new DateTime(2026, 5, 4, 13, 45, 30, 101), context.Events.Select(e => e.Updated).First());
77+
}
78+
79+
[TestMethod]
80+
public void Where_FiltersByDateTimeEquality()
81+
{
82+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
83+
84+
var target = new DateTime(2026, 5, 4, 13, 45, 30);
85+
context.Events.AddRange(
86+
new Event { Id = 1, CreatedAt = new DateTime(2026, 5, 4, 12, 0, 0) },
87+
new Event { Id = 2, CreatedAt = target },
88+
new Event { Id = 3, CreatedAt = new DateTime(2026, 5, 4, 15, 0, 0) });
89+
_ = context.SaveChanges();
90+
91+
var match = context.Events.Where(e => e.CreatedAt == target).Select(e => e.Id).Single();
92+
Assert.AreEqual(2, match);
93+
}
94+
95+
[TestMethod]
96+
public void MultipleRows_RoundTripBothColumns()
97+
{
98+
using var context = new TestDbContext(TestDbContext.CreateEventsSimulation());
99+
100+
var a = new DateTime(2026, 5, 4, 13, 45, 30);
101+
var b = new DateTime(2026, 5, 4, 14, 0, 0);
102+
context.Events.AddRange(
103+
new Event { Id = 1, CreatedAt = a, Updated = b },
104+
new Event { Id = 2, CreatedAt = b, Updated = null });
105+
_ = context.SaveChanges();
106+
107+
var rows = context.Events.OrderBy(e => e.Id).Select(e => new { e.CreatedAt, e.Updated }).ToArray();
108+
Assert.AreEqual(2, rows.Length);
109+
Assert.AreEqual(a, rows[0].CreatedAt);
110+
Assert.AreEqual(b, rows[0].Updated);
111+
Assert.AreEqual(b, rows[1].CreatedAt);
112+
Assert.IsNull(rows[1].Updated);
113+
}
114+
}

SqlServerSimulator.Tests.EFCore/TestDbContext.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ internal class Person
2828
public byte[]? Avatar { get; set; }
2929
}
3030

31+
/// <summary>
32+
/// Exercises the simulator's <c>datetime2</c> column support through EF Core.
33+
/// Properties are <see cref="DateTime"/> rather than <see cref="DateOnly"/> /
34+
/// <see cref="TimeOnly"/> / <see cref="TimeSpan"/> because EF Core's SqlServer
35+
/// provider downcasts <see cref="System.Data.Common.DbParameter"/> to
36+
/// <c>SqlParameter</c> in those mappings; <c>DateTime → datetime2</c> is the
37+
/// only date/time configuration whose mapping leaves the default
38+
/// <c>SqlDbType</c> in place and so doesn't trigger the downcast.
39+
/// See <see cref="SimulatedDbParameter"/> for the full compatibility matrix.
40+
/// </summary>
41+
internal class Event
42+
{
43+
public int Id { get; set; }
44+
45+
[Column(TypeName = "datetime2(7)")]
46+
public DateTime CreatedAt { get; set; }
47+
48+
[Column(TypeName = "datetime2(3)")]
49+
public DateTime? Updated { get; set; }
50+
}
51+
3152
internal class TestDbContext(Simulation simulation) : DbContext
3253
{
3354
public Simulation Simulation { get; set; } = simulation;
@@ -46,6 +67,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
4667

4768
public DbSet<Person> People => Set<Person>();
4869

70+
public DbSet<Event> Events => Set<Event>();
71+
4972
public static Simulation CreateDefaultSimulation(params ReadOnlySpan<int> values)
5073
{
5174
var simulation = new Simulation();
@@ -77,4 +100,14 @@ public static Simulation CreatePeopleSimulation()
77100
.ExecuteNonQuery();
78101
return simulation;
79102
}
103+
104+
public static Simulation CreateEventsSimulation()
105+
{
106+
var simulation = new Simulation();
107+
_ = simulation
108+
.CreateOpenConnection()
109+
.CreateCommand("create table Events ( Id int, CreatedAt datetime2(7) not null, Updated datetime2(3) null )")
110+
.ExecuteNonQuery();
111+
return simulation;
112+
}
80113
}

SqlServerSimulator.Tests.Internal/Storage/DecodeColumnTests.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
namespace SqlServerSimulator.Storage;
44

55
/// <summary>
6-
/// Unit tests for <see cref="RowDecoder.DecodeColumn"/>, which the data reader
7-
/// uses to navigate row bytes one column at a time without materializing the
8-
/// whole row.
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
99
/// </summary>
10+
/// <remarks>
11+
/// Covers <see cref="RowDecoder.DecodeColumn"/>, which the data reader uses to
12+
/// navigate row bytes one column at a time without materializing the whole row.
13+
/// </remarks>
1014
[TestClass]
1115
public sealed class DecodeColumnTests
1216
{

SqlServerSimulator.Tests.Internal/Storage/HeapPageTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
namespace SqlServerSimulator.Storage;
55

6+
/// <summary>
7+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
8+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
9+
/// catch regressions the way users will.
10+
/// </summary>
611
[TestClass]
712
public class HeapPageTests
813
{

SqlServerSimulator.Tests.Internal/Storage/HeapTests.cs

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

33
namespace SqlServerSimulator.Storage;
44

5+
/// <summary>
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
9+
/// </summary>
510
[TestClass]
611
public class HeapTests
712
{

SqlServerSimulator.Tests.Internal/Storage/MixedTypeRowTests.cs

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

33
namespace SqlServerSimulator.Storage;
44

5+
/// <summary>
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
9+
/// </summary>
510
[TestClass]
611
public class MixedTypeRowTests
712
{

SqlServerSimulator.Tests.Internal/Storage/RowRoundTripTests.cs

Lines changed: 5 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
namespace SqlServerSimulator.Storage;
44

5+
/// <summary>
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
9+
/// </summary>
510
[TestClass]
611
public class RowRoundTripTests
712
{
@@ -184,28 +189,6 @@ public void Decoder_RejectsFixedEndMismatch()
184189
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
185190
}
186191

187-
[TestMethod]
188-
[DataRow(1, 1, 1)] // 0001-01-01: minimum
189-
[DataRow(9999, 12, 31)] // 9999-12-31: maximum
190-
[DataRow(2026, 5, 4)]
191-
[DataRow(1900, 1, 1)]
192-
public void SingleColumn_Date_RoundTrips(int year, int month, int day)
193-
{
194-
var v = SqlValue.FromDate(new DateOnly(year, month, day));
195-
var decoded = RowDecoder.DecodeRow([SqlType.Date], RowEncoder.EncodeRow([SqlType.Date], [v]));
196-
AreEqual(1, decoded.Length);
197-
AreEqual(v, decoded[0]);
198-
}
199-
200-
[TestMethod]
201-
public void SingleColumn_DateNull_RoundTrips()
202-
{
203-
var decoded = RowDecoder.DecodeRow([SqlType.Date], RowEncoder.EncodeRow([SqlType.Date], [SqlValue.Null(SqlType.Date)]));
204-
AreEqual(1, decoded.Length);
205-
IsTrue(decoded[0].IsNull);
206-
AreSame(SqlType.Date, decoded[0].Type);
207-
}
208-
209192
[TestMethod]
210193
public void EncodedSingleColumnDateRow_HasLength10()
211194
{
@@ -221,57 +204,4 @@ public void EncodedDateRow_StoresDayCountLittleEndian()
221204
AreEqual(7, BitConverter.ToUInt16(bytes, 2)); // fixed-length end offset (4 + 3)
222205
CollectionAssert.AreEqual(new byte[] { 0x9B, 0x49, 0x0B }, bytes[4..7]);
223206
}
224-
225-
[TestMethod]
226-
[DataRow(0, 6)] // precision 0-2: 6 bytes (3 time + 3 date)
227-
[DataRow(1, 6)]
228-
[DataRow(2, 6)]
229-
[DataRow(3, 7)] // precision 3-4: 7 bytes (4 time + 3 date)
230-
[DataRow(4, 7)]
231-
[DataRow(5, 8)] // precision 5-7: 8 bytes (5 time + 3 date)
232-
[DataRow(6, 8)]
233-
[DataRow(7, 8)]
234-
public void DateTime2_FixedLength_FollowsPrecisionTier(int precision, int expectedBytes) =>
235-
AreEqual(expectedBytes, SqlType.GetDateTime2(precision).FixedLength);
236-
237-
[TestMethod]
238-
[DataRow(0)]
239-
[DataRow(3)]
240-
[DataRow(7)]
241-
public void SingleColumn_DateTime2_RoundTrips(int precision)
242-
{
243-
var type = SqlType.GetDateTime2(precision);
244-
var v = SqlValue.FromDateTime2(type, new DateTime(2026, 5, 4, 13, 45, 30));
245-
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [v]));
246-
AreEqual(1, decoded.Length);
247-
AreEqual(v, decoded[0]);
248-
}
249-
250-
[TestMethod]
251-
public void SingleColumn_DateTime2_PreservesAllFractionalTicks()
252-
{
253-
var type = SqlType.GetDateTime2(7);
254-
var v = SqlValue.FromDateTime2(type, new DateTime(2026, 5, 4, 13, 45, 30).AddTicks(1234567));
255-
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [v]));
256-
AreEqual(v, decoded[0]);
257-
}
258-
259-
[TestMethod]
260-
public void SingleColumn_DateTime2Null_RoundTrips()
261-
{
262-
var type = SqlType.GetDateTime2(7);
263-
var decoded = RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [SqlValue.Null(type)]));
264-
IsTrue(decoded[0].IsNull);
265-
AreSame(type, decoded[0].Type);
266-
}
267-
268-
[TestMethod]
269-
public void SingleColumn_DateTime2_AtRangeBoundaries()
270-
{
271-
var type = SqlType.GetDateTime2(7);
272-
var min = SqlValue.FromDateTime2(type, DateTime.MinValue);
273-
var max = SqlValue.FromDateTime2(type, new DateTime(9999, 12, 31, 23, 59, 59).AddTicks(9_999_999));
274-
AreEqual(min, RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [min]))[0]);
275-
AreEqual(max, RowDecoder.DecodeRow([type], RowEncoder.EncodeRow([type], [max]))[0]);
276-
}
277207
}

SqlServerSimulator.Tests.Internal/Storage/SqlValueTests.cs

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

33
namespace SqlServerSimulator.Storage;
44

5+
/// <summary>
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
9+
/// </summary>
510
[TestClass]
611
public class SqlValueTests
712
{
@@ -191,4 +196,17 @@ public void DateTime2_DifferentPrecisions_AreDistinctTypes()
191196
AreNotSame(v3.Type, v7.Type);
192197
AreNotEqual(v3, v7);
193198
}
199+
200+
[TestMethod]
201+
public void FromTime_RejectsNonTimeType() =>
202+
Throws<ArgumentException>(() => SqlValue.FromTime(SqlType.Int32, TimeSpan.Zero));
203+
204+
[TestMethod]
205+
public void Time_DifferentPrecisions_AreDistinctTypes()
206+
{
207+
var v3 = SqlValue.FromTime(SqlType.GetTime(3), new TimeSpan(13, 45, 30));
208+
var v7 = SqlValue.FromTime(SqlType.GetTime(7), new TimeSpan(13, 45, 30));
209+
AreNotSame(v3.Type, v7.Type);
210+
AreNotEqual(v3, v7);
211+
}
194212
}

SqlServerSimulator.Tests.Internal/Storage/VarLengthRowTests.cs

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

33
namespace SqlServerSimulator.Storage;
44

5+
/// <summary>
6+
/// Internal-only tests. If a behavior is reachable through SQL, write it in
7+
/// SqlServerSimulator.Tests instead — public-API tests survive refactors and
8+
/// catch regressions the way users will.
9+
/// </summary>
510
[TestClass]
611
public class VarLengthRowTests
712
{

0 commit comments

Comments
 (0)