Skip to content

Commit 63893fc

Browse files
committed
Support the "date" type.
1 parent 2c2a6ac commit 63893fc

12 files changed

Lines changed: 436 additions & 7 deletions

File tree

SqlServerSimulator.Tests.Internal/Storage/RowRoundTripTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,42 @@ public void Decoder_RejectsFixedEndMismatch()
183183
bytes[2] = 0x09;
184184
_ = Throws<InvalidDataException>(() => RowDecoder.DecodeRow([SqlType.Int32], bytes));
185185
}
186+
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+
209+
[TestMethod]
210+
public void EncodedSingleColumnDateRow_HasLength10()
211+
{
212+
// 4 (header) + 3 (one Date) + 2 (column count) + 1 (NULL bitmap byte) = 10.
213+
AreEqual(10, RowEncoder.EncodeRow([SqlType.Date], [SqlValue.FromDate(new(2026, 5, 4))]).Length);
214+
}
215+
216+
[TestMethod]
217+
public void EncodedDateRow_StoresDayCountLittleEndian()
218+
{
219+
// 2026-05-04 is DateOnly.DayNumber = 739739 = 0x0B499B. Little-endian: 9B 49 0B.
220+
var bytes = RowEncoder.EncodeRow([SqlType.Date], [SqlValue.FromDate(new(2026, 5, 4))]);
221+
AreEqual(7, BitConverter.ToUInt16(bytes, 2)); // fixed-length end offset (4 + 3)
222+
CollectionAssert.AreEqual(new byte[] { 0x9B, 0x49, 0x0B }, bytes[4..7]);
223+
}
186224
}

SqlServerSimulator.Tests.Internal/Storage/SqlValueTests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Data.Common;
12
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
23

34
namespace SqlServerSimulator.Storage;
@@ -168,4 +169,113 @@ public void CoerceTo_BigIntToInt32_OutOfRangeOverflows() =>
168169
[TestMethod]
169170
public void CoerceTo_BetweenStringAndInt_NotSupported() =>
170171
Throws<NotSupportedException>(() => SqlValue.FromVarchar("42").CoerceTo(SqlType.Int32));
172+
173+
[TestMethod]
174+
public void FromDate_RoundTripsViaAsDate()
175+
{
176+
var v = SqlValue.FromDate(new DateOnly(2026, 5, 4));
177+
IsFalse(v.IsNull);
178+
AreSame(SqlType.Date, v.Type);
179+
AreEqual(new DateOnly(2026, 5, 4), v.AsDate);
180+
}
181+
182+
[TestMethod]
183+
[DataRow(1, 1, 1)] // 0001-01-01: minimum
184+
[DataRow(9999, 12, 31)] // 9999-12-31: maximum
185+
[DataRow(2026, 5, 4)]
186+
[DataRow(1900, 1, 1)]
187+
public void FromDate_PreservesAcrossSupportedRange(int year, int month, int day)
188+
{
189+
var d = new DateOnly(year, month, day);
190+
AreEqual(d, SqlValue.FromDate(d).AsDate);
191+
}
192+
193+
[TestMethod]
194+
public void Date_Equals_ComparesDayValue()
195+
{
196+
AreEqual(SqlValue.FromDate(new DateOnly(2026, 5, 4)), SqlValue.FromDate(new DateOnly(2026, 5, 4)));
197+
AreNotEqual(SqlValue.FromDate(new DateOnly(2026, 5, 4)), SqlValue.FromDate(new DateOnly(2026, 5, 5)));
198+
}
199+
200+
[TestMethod]
201+
public void Date_CompareTo_OrdersChronologically()
202+
{
203+
var earlier = SqlValue.FromDate(new DateOnly(2024, 1, 1));
204+
var later = SqlValue.FromDate(new DateOnly(2026, 12, 31));
205+
IsTrue(earlier.CompareTo(later) < 0);
206+
IsTrue(later.CompareTo(earlier) > 0);
207+
AreEqual(0, earlier.CompareTo(earlier));
208+
}
209+
210+
[TestMethod]
211+
public void Date_ToObject_ReturnsDateTimeAtMidnight()
212+
{
213+
var v = SqlValue.FromDate(new DateOnly(2026, 5, 4));
214+
var obj = v.ToObject();
215+
var dt = IsInstanceOfType<DateTime>(obj);
216+
AreEqual(new DateTime(2026, 5, 4, 0, 0, 0), dt);
217+
}
218+
219+
[TestMethod]
220+
public void Date_NullToObject_ReturnsNull()
221+
{
222+
var v = SqlValue.Null(SqlType.Date);
223+
IsNull(v.ToObject());
224+
}
225+
226+
[TestMethod]
227+
public void Date_AsDate_OnNull_Throws() =>
228+
Throws<InvalidOperationException>(() => SqlValue.Null(SqlType.Date).AsDate);
229+
230+
[TestMethod]
231+
public void Date_AsDate_OnWrongType_Throws() =>
232+
Throws<InvalidOperationException>(() => SqlValue.FromInt32(1).AsDate);
233+
234+
[TestMethod]
235+
[DataRow("2026-05-04", 2026, 5, 4)]
236+
[DataRow("20260504", 2026, 5, 4)]
237+
[DataRow("2026-05-04T13:45:30", 2026, 5, 4)]
238+
[DataRow("2026-05-04T13:45:30.1234567", 2026, 5, 4)]
239+
[DataRow("0001-01-01", 1, 1, 1)]
240+
[DataRow("9999-12-31", 9999, 12, 31)]
241+
public void CoerceTo_VarcharToDate_ParsesIsoForms(string text, int year, int month, int day)
242+
{
243+
var coerced = SqlValue.FromVarchar(text).CoerceTo(SqlType.Date);
244+
AreSame(SqlType.Date, coerced.Type);
245+
AreEqual(new DateOnly(year, month, day), coerced.AsDate);
246+
}
247+
248+
[TestMethod]
249+
public void CoerceTo_NVarcharToDate_ParsesIsoForms()
250+
{
251+
var coerced = SqlValue.FromNVarchar("2026-05-04").CoerceTo(SqlType.Date);
252+
AreEqual(new DateOnly(2026, 5, 4), coerced.AsDate);
253+
}
254+
255+
[TestMethod]
256+
[DataRow("not a date")]
257+
[DataRow("")]
258+
[DataRow("2026-13-01")] // invalid month
259+
[DataRow("Jan 15, 2026")] // locale-sensitive form not handled
260+
public void CoerceTo_BadStringToDate_RaisesConversionFailed(string text)
261+
{
262+
var ex = Throws<DbException>(() => SqlValue.FromVarchar(text).CoerceTo(SqlType.Date));
263+
AreEqual("Conversion failed when converting date and/or time from character string.", ex.Message);
264+
}
265+
266+
[TestMethod]
267+
public void CoerceTo_DateToVarchar_FormatsIso()
268+
{
269+
var coerced = SqlValue.FromDate(new DateOnly(2026, 5, 4)).CoerceTo(SqlType.Varchar);
270+
AreSame(SqlType.Varchar, coerced.Type);
271+
AreEqual("2026-05-04", coerced.AsString);
272+
}
273+
274+
[TestMethod]
275+
public void CoerceTo_DateToNVarchar_FormatsIso()
276+
{
277+
var coerced = SqlValue.FromDate(new DateOnly(2026, 5, 4)).CoerceTo(SqlType.NVarchar);
278+
AreSame(SqlType.NVarchar, coerced.Type);
279+
AreEqual("2026-05-04", coerced.AsString);
280+
}
171281
}

SqlServerSimulator.Tests/BuiltInFunctionTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,35 @@ public void Cast_CrossCategory_NotSupported()
164164
// NotSupportedException SqlValue.CoerceTo throws.
165165
_ = Throws<NotSupportedException>(() => ExecuteScalar("select cast('42' as int)"));
166166
}
167+
168+
[TestMethod]
169+
[DataRow("cast('2026-05-04' as date)", "2026-05-04")]
170+
[DataRow("cast('20260504' as date)", "2026-05-04")]
171+
[DataRow("cast('2026-05-04T13:45:30' as date)", "2026-05-04")]
172+
[DataRow("cast(N'2026-05-04' as date)", "2026-05-04")]
173+
public void Cast_StringToDate(string expression, string expectedIso)
174+
{
175+
var value = ExecuteScalar($"select {expression}");
176+
var dt = IsInstanceOfType<DateTime>(value);
177+
AreEqual(DateTime.Parse(expectedIso, System.Globalization.CultureInfo.InvariantCulture), dt);
178+
}
179+
180+
[TestMethod]
181+
[DataRow("not a date")]
182+
[DataRow("2026-13-01")]
183+
public void Cast_BadStringToDate_RaisesConversionFailed(string text)
184+
{
185+
var ex = Throws<DbException>(() => ExecuteScalar($"select cast('{text}' as date)"));
186+
AreEqual("Conversion failed when converting date and/or time from character string.", ex.Message);
187+
}
188+
189+
[TestMethod]
190+
[DataRow("cast(cast('2026-05-04' as date) as varchar(10))", "2026-05-04")]
191+
[DataRow("cast(cast('2026-05-04' as date) as nvarchar(10))", "2026-05-04")]
192+
public void Cast_DateToString(string expression, string expected) =>
193+
AreEqual(expected, ExecuteScalar($"select {expression}"));
194+
195+
[TestMethod]
196+
public void Cast_DateNullPassesThrough() =>
197+
IsInstanceOfType<DBNull>(ExecuteScalar("select cast(cast(null as date) as varchar(10))"));
167198
}

SqlServerSimulator.Tests/CreateTableTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,33 @@ public void CreateTableFixedWidthSumAtRowSizeMax()
227227
connection.Open();
228228
Assert.AreEqual(-1, command.ExecuteNonQuery());
229229
}
230+
231+
[TestMethod]
232+
[DataRow("date")]
233+
[DataRow("DATE")]
234+
[DataRow("Date")]
235+
public void CreateTableDate(string typeSpec)
236+
{
237+
var simulation = new Simulation();
238+
239+
using var connection = simulation.CreateDbConnection();
240+
using var command = connection.CreateCommand($"create table t ( v {typeSpec} )");
241+
242+
connection.Open();
243+
Assert.AreEqual(-1, command.ExecuteNonQuery());
244+
}
245+
246+
[TestMethod]
247+
public void CreateTableDateRejectsLengthSpecifier()
248+
{
249+
// date is fixed-length; SQL Server rejects date(N) with Msg 2716.
250+
var simulation = new Simulation();
251+
252+
using var connection = simulation.CreateDbConnection();
253+
using var command = connection.CreateCommand("create table t ( v date(3) )");
254+
255+
connection.Open();
256+
var x = Assert.Throws<DbException>(() => command.ExecuteNonQuery());
257+
Assert.AreEqual("Column, parameter, or variable #1: Cannot specify a column width on data type date.", x.Message);
258+
}
230259
}

SqlServerSimulator.Tests/InsertTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,66 @@ public void InsertNullIntoConstrainedVarbinary_Succeeds()
295295
Assert.AreEqual(1, insert.ExecuteNonQuery());
296296
}
297297

298+
[TestMethod]
299+
public void InsertDate_ViaParameter_RoundTrips()
300+
{
301+
using var connection = new Simulation().CreateOpenConnection();
302+
_ = connection.CreateCommand("create table t ( d date )").ExecuteNonQuery();
303+
304+
using var insert = connection.CreateCommand();
305+
insert.CommandText = "insert t values ( @p )";
306+
AddTypedParameter(insert, "p", DbType.Date, new DateOnly(2026, 5, 4));
307+
Assert.AreEqual(1, insert.ExecuteNonQuery());
308+
309+
var read = connection.CreateCommand("select d from t").ExecuteScalar();
310+
Assert.AreEqual(new DateTime(2026, 5, 4), read);
311+
}
312+
313+
[TestMethod]
314+
public void InsertDate_ViaDateTimeParameter_RoundTrips()
315+
{
316+
// EF Core's legacy DateTime mapping arrives as DbType.Date with a
317+
// DateTime value; only the date portion should land in storage.
318+
using var connection = new Simulation().CreateOpenConnection();
319+
_ = connection.CreateCommand("create table t ( d date )").ExecuteNonQuery();
320+
321+
using var insert = connection.CreateCommand();
322+
insert.CommandText = "insert t values ( @p )";
323+
AddTypedParameter(insert, "p", DbType.Date, new DateTime(2026, 5, 4, 13, 45, 30));
324+
Assert.AreEqual(1, insert.ExecuteNonQuery());
325+
326+
var read = connection.CreateCommand("select d from t").ExecuteScalar();
327+
Assert.AreEqual(new DateTime(2026, 5, 4), read);
328+
}
329+
330+
[TestMethod]
331+
public void InsertDate_NullValue_RoundTrips()
332+
{
333+
using var connection = new Simulation().CreateOpenConnection();
334+
_ = connection.CreateCommand("create table t ( d date null )").ExecuteNonQuery();
335+
336+
using var insert = connection.CreateCommand();
337+
insert.CommandText = "insert t values ( @p )";
338+
AddTypedParameter(insert, "p", DbType.Date, DBNull.Value);
339+
Assert.AreEqual(1, insert.ExecuteNonQuery());
340+
341+
var read = connection.CreateCommand("select d from t").ExecuteScalar();
342+
Assert.AreEqual(DBNull.Value, read);
343+
}
344+
345+
[TestMethod]
346+
public void InsertDate_AutoDetectedDbType_RoundTrips()
347+
{
348+
// SimulatedDbParameter infers DbType.Date when Value is a DateOnly.
349+
using var connection = new Simulation().CreateOpenConnection();
350+
_ = connection.CreateCommand("create table t ( d date )").ExecuteNonQuery();
351+
352+
using var insert = connection.CreateCommand("insert t values ( @p )", ("p", new DateOnly(2026, 5, 4)));
353+
Assert.AreEqual(1, insert.ExecuteNonQuery());
354+
355+
Assert.AreEqual(new DateTime(2026, 5, 4), connection.CreateCommand("select d from t").ExecuteScalar());
356+
}
357+
298358
private static void AddTypedParameter(DbCommand command, string name, DbType dbType, object value)
299359
{
300360
var parameter = command.CreateParameter();

SqlServerSimulator.Tests/WhereTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,68 @@ public void FromTableWhere_FiltersByParameter()
116116
IsFalse(reader.Read());
117117
}
118118

119+
[TestMethod]
120+
public void FromTableWhere_FiltersByDateEquality()
121+
{
122+
using var connection = new Simulation().CreateOpenConnection();
123+
124+
_ = connection.CreateCommand("create table t ( id int, d date )").ExecuteNonQuery();
125+
126+
using var ins = connection.CreateCommand();
127+
ins.CommandText = "insert t values ( 1, @a ), ( 2, @b ), ( 3, @c )";
128+
AddTypedParameter(ins, "a", DbType.Date, new DateOnly(2024, 1, 1));
129+
AddTypedParameter(ins, "b", DbType.Date, new DateOnly(2026, 5, 4));
130+
AddTypedParameter(ins, "c", DbType.Date, new DateOnly(2099, 12, 31));
131+
_ = ins.ExecuteNonQuery();
132+
133+
using var select = connection.CreateCommand();
134+
select.CommandText = "select id from t where d = @target";
135+
AddTypedParameter(select, "target", DbType.Date, new DateOnly(2026, 5, 4));
136+
137+
using var reader = select.ExecuteReader();
138+
IsTrue(reader.Read());
139+
AreEqual(2, reader[0]);
140+
IsFalse(reader.Read());
141+
}
142+
143+
[TestMethod]
144+
public void FromTableWhere_FiltersByDateOrdering()
145+
{
146+
using var connection = new Simulation().CreateOpenConnection();
147+
148+
_ = connection.CreateCommand("create table t ( id int, d date )").ExecuteNonQuery();
149+
150+
using var ins = connection.CreateCommand();
151+
ins.CommandText = "insert t values ( 1, @a ), ( 2, @b ), ( 3, @c )";
152+
AddTypedParameter(ins, "a", DbType.Date, new DateOnly(2024, 1, 1));
153+
AddTypedParameter(ins, "b", DbType.Date, new DateOnly(2026, 5, 4));
154+
AddTypedParameter(ins, "c", DbType.Date, new DateOnly(2099, 12, 31));
155+
_ = ins.ExecuteNonQuery();
156+
157+
using var select = connection.CreateCommand();
158+
select.CommandText = "select id from t where d > @target";
159+
AddTypedParameter(select, "target", DbType.Date, new DateOnly(2026, 1, 1));
160+
161+
using var reader = select.ExecuteReader();
162+
IsTrue(reader.Read()); AreEqual(2, reader[0]);
163+
IsTrue(reader.Read()); AreEqual(3, reader[0]);
164+
IsFalse(reader.Read());
165+
}
166+
167+
[TestMethod]
168+
public void FromTableWhere_DateColumnReadableViaGetDateTime()
169+
{
170+
using var connection = new Simulation().CreateOpenConnection();
171+
_ = connection.CreateCommand("create table t ( d date )").ExecuteNonQuery();
172+
173+
using var ins = connection.CreateCommand("insert t values ( @p )", ("p", new DateOnly(2026, 5, 4)));
174+
_ = ins.ExecuteNonQuery();
175+
176+
using var reader = connection.CreateCommand("select d from t").ExecuteReader();
177+
IsTrue(reader.Read());
178+
AreEqual(new DateTime(2026, 5, 4), reader.GetDateTime(0));
179+
}
180+
119181
private static void AddTypedParameter(System.Data.Common.DbCommand command, string name, DbType dbType, object value)
120182
{
121183
var parameter = command.CreateParameter();

SqlServerSimulator/Parser/ParserContext.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ private static SqlValue ConvertParameter(object? raw, SqlType type) =>
5252
: type == SqlType.NVarchar ? SqlValue.FromNVarchar((string)raw)
5353
: type == SqlType.SystemName ? SqlValue.FromSystemName((string)raw)
5454
: type == SqlType.Varbinary ? SqlValue.FromVarbinary((byte[])raw)
55+
// EF Core's mapping for DateOnly arrives as DbType.Date with a DateOnly
56+
// value; the legacy DateTime mapping arrives as DbType.Date with a
57+
// DateTime value (date portion only).
58+
: type == SqlType.Date ? SqlValue.FromDate(raw switch
59+
{
60+
DateOnly d => d,
61+
DateTime dt => DateOnly.FromDateTime(dt),
62+
_ => throw new NotSupportedException($"Don't know how to convert raw parameter value of type {raw.GetType().Name} to {type}."),
63+
})
5564
: throw new NotSupportedException($"Don't know how to convert raw parameter value of type {raw.GetType().Name} to {type}.");
5665

5766
public Simulation Simulation => Command.simulation;

SqlServerSimulator/SimulatedDbDataReader.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@ public override string GetDataTypeName(int ordinal)
6060
throw new NotImplementedException();
6161
}
6262

63-
public override DateTime GetDateTime(int ordinal)
64-
{
65-
throw new NotImplementedException();
66-
}
63+
public override DateTime GetDateTime(int ordinal) => (DateTime)this[ordinal];
6764

6865
public override decimal GetDecimal(int ordinal)
6966
{

0 commit comments

Comments
 (0)